Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions invokeai/app/api/routers/node_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from __future__ import annotations

import importlib.resources as pkg_resources
import mimetypes
import re
from importlib import import_module

from fastapi import APIRouter, HTTPException
from fastapi.responses import PlainTextResponse, Response

from invokeai.backend.util.logging import InvokeAILogger

logger = InvokeAILogger.get_logger(__name__)

router = APIRouter()

# validation regexes
VALID_SEGMENT = re.compile(r"^[A-Za-z0-9_-]+$")
IMAGE_SEGMENT = re.compile(r"^[A-Za-z0-9_.-]+$")


@router.get("/nodeDocs/{lang}/{name}.md")
def get_node_doc(lang: str, name: str) -> PlainTextResponse:
"""Return packaged markdown for a node.

This endpoint reads packaged resources from the installed `invokeai.resources`
package via importlib.
"""
# Basic validation
if not VALID_SEGMENT.match(lang) or not VALID_SEGMENT.match(name):
raise HTTPException(status_code=400, detail="Invalid path segment")

try:
res_pkg = import_module("invokeai.resources")
pkg_path = pkg_resources.files(res_pkg).joinpath("node_docs", lang, f"{name}.md")
except Exception as e:
logger.warning(f"node_docs: unable to import packaged resources: {e}")
raise HTTPException(status_code=404, detail="Not found")

# Ensure resource exists in the package
try:
if not pkg_path.is_file():
logger.debug(f"node_docs: resource not found in package: {pkg_path}")
raise FileNotFoundError
text = pkg_path.read_text(encoding="utf-8")
return PlainTextResponse(content=text, media_type="text/markdown")
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Not found")
except Exception as e:
logger.warning(f"node_docs: failed reading resource {pkg_path}: {e}")
raise HTTPException(status_code=404, detail="Not found")


@router.get("/nodeDocs/{lang}/images/{image_name}")
def get_node_doc_image(lang: str, image_name: str) -> Response:
"""Return packaged image resource for node docs.

Only reads from `invokeai.resources` packaged data. Adds `X-Content-Type-Options`
and a conservative Cache-Control header.
"""
# Validate inputs
if not IMAGE_SEGMENT.match(image_name) or not VALID_SEGMENT.match(lang):
raise HTTPException(status_code=400, detail="Invalid path segment")

try:
res_pkg = import_module("invokeai.resources")
pkg_path = pkg_resources.files(res_pkg).joinpath("node_docs", lang, "images", image_name)
except Exception as e:
logger.warning(f"node_docs: unable to import packaged resources for image: {e}")
raise HTTPException(status_code=404, detail="Not found")

try:
if not pkg_path.is_file():
logger.debug(f"node_docs: image resource not found in package: {pkg_path}")
raise FileNotFoundError
data = pkg_path.read_bytes()
mime_type, _ = mimetypes.guess_type(image_name)
headers = {
"X-Content-Type-Options": "nosniff",
"Cache-Control": "public, max-age=86400",
}
return Response(content=data, media_type=mime_type or "application/octet-stream", headers=headers)
except FileNotFoundError:
raise HTTPException(status_code=404, detail="Not found")
except Exception as e:
logger.warning(f"node_docs: failed reading image resource {pkg_path}: {e}")
raise HTTPException(status_code=404, detail="Not found")


# Expose the router
node_docs_router = router
4 changes: 4 additions & 0 deletions invokeai/app/api_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
images,
model_manager,
model_relationships,
# node_docs router serves packaged node documentation
node_docs,
session_queue,
style_presets,
utilities,
Expand Down Expand Up @@ -133,6 +135,8 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
app.include_router(workflows.workflows_router, prefix="/api")
app.include_router(style_presets.style_presets_router, prefix="/api")
app.include_router(client_state.client_state_router, prefix="/api")
# Include node docs router (serves packaged node docs)
app.include_router(node_docs.node_docs_router, prefix="")

app.openapi = get_openapi_func(app)

Expand Down
3 changes: 3 additions & 0 deletions invokeai/frontend/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"cmdk": "^1.1.1",
"compare-versions": "^6.1.1",
"dockview": "^4.7.1",
"dompurify": "^2.4.0",
"es-toolkit": "^1.39.7",
"filesize": "^10.1.6",
"fracturedjsonjs": "^4.1.0",
Expand All @@ -69,6 +70,7 @@
"linkify-react": "^4.3.1",
"linkifyjs": "^4.3.1",
"lru-cache": "^11.1.0",
"marked": "^17.0.1",
"mtwist": "^1.0.2",
"nanoid": "^5.1.5",
"nanostores": "^1.0.1",
Expand Down Expand Up @@ -114,6 +116,7 @@
"@storybook/addon-docs": "^9.0.17",
"@storybook/addon-links": "^9.0.17",
"@storybook/react-vite": "^9.0.17",
"@types/dompurify": "^3.2.0",
"@types/node": "^22.15.1",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.0",
Expand Down
52 changes: 48 additions & 4 deletions invokeai/frontend/web/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1274,7 +1274,9 @@
"alignmentDL": "Bottom Left",
"alignmentUR": "Top Right",
"alignmentDR": "Bottom Right"
}
},
"help": "Help",
"noDocsAvailable": "No documentation available for this node."
},
"parameters": {
"aspect": "Aspect",
Expand Down Expand Up @@ -1561,8 +1563,6 @@
"problemUnpublishingWorkflow": "Problem Unpublishing Workflow",
"problemUnpublishingWorkflowDescription": "There was a problem unpublishing the workflow. Please try again.",
"workflowUnpublished": "Workflow Unpublished",
"sentToCanvas": "Sent to Canvas",
"sentToUpscale": "Sent to Upscale",
"promptGenerationStarted": "Prompt generation started",
"uploadAndPromptGenerationFailed": "Failed to upload image and generate prompt",
"promptExpansionFailed": "We ran into an issue. Please try prompt expansion again.",
Expand Down Expand Up @@ -2685,7 +2685,6 @@
"selectPreset": "Select Style Preset",
"noMatchingPresets": "No matching presets"
},

"ui": {
"tabs": {
"generate": "Generate",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useNodeHasErrors } from 'features/nodes/hooks/useNodeIsInvalid';
import { memo } from 'react';

import InvocationNodeCollapsedHandles from './InvocationNodeCollapsedHandles';
import { InvocationNodeHelpButton } from './InvocationNodeHelpButton';
import { InvocationNodeInfoIcon } from './InvocationNodeInfoIcon';
import InvocationNodeStatusIndicator from './InvocationNodeStatusIndicator';

Expand Down Expand Up @@ -38,6 +39,7 @@ const InvocationNodeHeader = ({ nodeId, isOpen }: Props) => {
<InvocationNodeTitle nodeId={nodeId} />
<Flex alignItems="center">
<InvocationNodeStatusIndicator nodeId={nodeId} />
<InvocationNodeHelpButton nodeId={nodeId} />
<InvocationNodeInfoIcon nodeId={nodeId} />
</Flex>
{!isOpen && <InvocationNodeCollapsedHandles nodeId={nodeId} />}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Icon, useDisclosure } from '@invoke-ai/ui-library';
import { memo } from 'react';
import { PiQuestionBold } from 'react-icons/pi';

import { InvocationNodeHelpModal } from './InvocationNodeHelpModal';

interface Props {
nodeId: string;
}

export const InvocationNodeHelpButton = memo(({ nodeId: _nodeId }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure();

return (
<>
<Icon
as={PiQuestionBold}
display="block"
boxSize={4}
w={8}
cursor="pointer"
onClick={onOpen}
_hover={{ color: 'base.300' }}
aria-label="Help"
/>
<InvocationNodeHelpModal isOpen={isOpen} onClose={onClose} />
</>
);
});

InvocationNodeHelpButton.displayName = 'InvocationNodeHelpButton';
Loading
Loading