add toc, markdown jump, default docs
This commit is contained in:
@@ -265,6 +265,28 @@ def create_app(
|
|||||||
content_type="text/plain",
|
content_type="text/plain",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def get_help_docs(request: web.Request) -> web.Response:
|
||||||
|
public_dir = FRONTEND_DIR / "public"
|
||||||
|
if not public_dir.is_dir():
|
||||||
|
return web.json_response([])
|
||||||
|
files = sorted(p.name for p in public_dir.iterdir() if p.suffix.lower() == ".md" and p.is_file())
|
||||||
|
result = []
|
||||||
|
for fname in files:
|
||||||
|
text = (public_dir / fname).read_text(encoding="utf-8", errors="replace")
|
||||||
|
title = fname.rsplit(".", 1)[0].replace("-", " ").replace("_", " ").title()
|
||||||
|
result.append({"title": title, "content": text})
|
||||||
|
return web.json_response(result)
|
||||||
|
|
||||||
|
async def get_help_doc_file(request: web.Request) -> web.Response:
|
||||||
|
filename = request.match_info["filename"]
|
||||||
|
public_dir = FRONTEND_DIR / "public"
|
||||||
|
path = (public_dir / filename).resolve()
|
||||||
|
if not str(path).startswith(str(public_dir.resolve())) or not path.is_file():
|
||||||
|
return web.Response(status=404, text="Not found")
|
||||||
|
text = path.read_text(encoding="utf-8", errors="replace")
|
||||||
|
title = filename.rsplit(".", 1)[0].replace("-", " ").replace("_", " ").title()
|
||||||
|
return web.json_response({"title": title, "content": text})
|
||||||
|
|
||||||
async def get_nodes(request: web.Request) -> web.Response:
|
async def get_nodes(request: web.Request) -> web.Response:
|
||||||
return web.Response(
|
return web.Response(
|
||||||
text=_dumps(get_all_node_info()),
|
text=_dumps(get_all_node_info()),
|
||||||
@@ -545,6 +567,8 @@ def create_app(
|
|||||||
app.router.add_post("/save-workflow-png", save_workflow_png)
|
app.router.add_post("/save-workflow-png", save_workflow_png)
|
||||||
app.router.add_get("/channels", get_channels)
|
app.router.add_get("/channels", get_channels)
|
||||||
app.router.add_get("/docs", get_node_doc)
|
app.router.add_get("/docs", get_node_doc)
|
||||||
|
app.router.add_get("/help-docs", get_help_docs)
|
||||||
|
app.router.add_get("/help-docs/{filename}", get_help_doc_file)
|
||||||
app.router.add_post("/prompt", submit_prompt)
|
app.router.add_post("/prompt", submit_prompt)
|
||||||
app.router.add_get("/ws", websocket_handler)
|
app.router.add_get("/ws", websocket_handler)
|
||||||
|
|
||||||
|
|||||||
@@ -1970,6 +1970,24 @@ function Flow() {
|
|||||||
setHelpTabs((prev) => prev.map((t) => t.label === label ? { ...t, content } : t));
|
setHelpTabs((prev) => prev.map((t) => t.label === label ? { ...t, content } : t));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const openDocByFilename = useCallback(async (filename) => {
|
||||||
|
const title = filename.replace(/\.md$/i, '').replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
|
// If already open, just switch to it
|
||||||
|
setHelpTabs((prev) => {
|
||||||
|
if (prev.find((t) => t.label === title)) return prev;
|
||||||
|
return [...prev, { label: title, content: null }];
|
||||||
|
});
|
||||||
|
setActiveHelpTab(title);
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/help-docs/${encodeURIComponent(filename)}`);
|
||||||
|
if (!r.ok) throw new Error('Not found');
|
||||||
|
const doc = await r.json();
|
||||||
|
setHelpTabs((prev) => prev.map((t) => t.label === title ? { ...t, content: doc.content } : t));
|
||||||
|
} catch {
|
||||||
|
setHelpTabs((prev) => prev.map((t) => t.label === title ? { ...t, content: `*Could not load ${filename}.*` } : t));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const contextValue = useMemo(() => ({
|
const contextValue = useMemo(() => ({
|
||||||
onWidgetChange,
|
onWidgetChange,
|
||||||
onRuntimeValuesChange,
|
onRuntimeValuesChange,
|
||||||
@@ -1996,9 +2014,18 @@ function Flow() {
|
|||||||
setEdges(hydrated.edges);
|
setEdges(hydrated.edges);
|
||||||
nextIdRef.current = hydrated.nextNodeId;
|
nextIdRef.current = hydrated.nextNodeId;
|
||||||
journalContentRef.current = data.journalContent || '';
|
journalContentRef.current = data.journalContent || '';
|
||||||
setHelpTabs((prev) => prev.map((t) =>
|
if (journalContentRef.current) {
|
||||||
t.label === 'Journal' ? { ...t, content: journalContentRef.current } : t,
|
setHelpTabs((prev) => {
|
||||||
));
|
const existing = prev.find((t) => t.label === 'Journal');
|
||||||
|
if (existing) return prev.map((t) => t.label === 'Journal' ? { ...t, content: journalContentRef.current } : t);
|
||||||
|
return [...prev, { label: 'Journal', type: 'journal', content: journalContentRef.current }];
|
||||||
|
});
|
||||||
|
setActiveHelpTab('Journal');
|
||||||
|
} else {
|
||||||
|
setHelpTabs((prev) => prev.map((t) =>
|
||||||
|
t.label === 'Journal' ? { ...t, content: '' } : t,
|
||||||
|
));
|
||||||
|
}
|
||||||
initializeDynamicNodes(hydrated.nodes);
|
initializeDynamicNodes(hydrated.nodes);
|
||||||
}, [initializeDynamicNodes, setNodes, setEdges]);
|
}, [initializeDynamicNodes, setNodes, setEdges]);
|
||||||
|
|
||||||
@@ -2038,6 +2065,20 @@ function Flow() {
|
|||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
setStatus({ text: 'Failed to load nodes: ' + err.message, level: 'error' });
|
setStatus({ text: 'Failed to load nodes: ' + err.message, level: 'error' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load any .md files from frontend/public/ as help tabs
|
||||||
|
fetch('/help-docs')
|
||||||
|
.then((r) => r.ok ? r.json() : [])
|
||||||
|
.then((docs) => {
|
||||||
|
if (!docs.length) return;
|
||||||
|
setHelpTabs((prev) => {
|
||||||
|
const existing = new Set(prev.map((t) => t.label));
|
||||||
|
const newTabs = docs.filter((d) => !existing.has(d.title)).map((d) => ({ label: d.title, content: d.content }));
|
||||||
|
return newTabs.length ? [...prev, ...newTabs] : prev;
|
||||||
|
});
|
||||||
|
setActiveHelpTab((cur) => cur || docs[0].title);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
}, [loadDefaultWorkflow]);
|
}, [loadDefaultWorkflow]);
|
||||||
|
|
||||||
const stampLogoOnBlob = useCallback(async (blob) => {
|
const stampLogoOnBlob = useCallback(async (blob) => {
|
||||||
@@ -2055,8 +2096,8 @@ function Flow() {
|
|||||||
ctx.drawImage(img, 0, 0);
|
ctx.drawImage(img, 0, 0);
|
||||||
|
|
||||||
const margin = 16;
|
const margin = 16;
|
||||||
const size = Math.min(128, Math.floor(img.naturalWidth / 6), Math.floor(img.naturalHeight / 6));
|
const size = 64;
|
||||||
if (size >= 16) {
|
if (img.naturalWidth >= size + margin * 2 && img.naturalHeight >= size + margin * 2) {
|
||||||
const logoX = img.naturalWidth - size - margin;
|
const logoX = img.naturalWidth - size - margin;
|
||||||
const logoY = img.naturalHeight - size - margin;
|
const logoY = img.naturalHeight - size - margin;
|
||||||
const fontSize = Math.max(11, Math.round(size * 0.18));
|
const fontSize = Math.max(11, Math.round(size * 0.18));
|
||||||
@@ -3011,6 +3052,7 @@ function Flow() {
|
|||||||
onTabClose={closeHelpTab}
|
onTabClose={closeHelpTab}
|
||||||
onTabContentChange={updateTabContent}
|
onTabContentChange={updateTabContent}
|
||||||
onOpenJournal={openJournalTab}
|
onOpenJournal={openJournalTab}
|
||||||
|
onOpenDoc={openDocByFilename}
|
||||||
/>
|
/>
|
||||||
</NodeContext.Provider>
|
</NodeContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,165 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
|
|
||||||
function JournalTab({ content, onChange }) {
|
// ── Parse headings from markdown source ──────────────────────────────
|
||||||
|
|
||||||
|
function parseHeadings(md) {
|
||||||
|
if (!md) return [];
|
||||||
|
const headings = [];
|
||||||
|
const lines = md.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
const m = line.match(/^(#{1,6})\s+(.+)/);
|
||||||
|
if (m) {
|
||||||
|
const text = m[2].replace(/[*_`~\[\]]/g, '').trim();
|
||||||
|
const id = text.toLowerCase().replace(/[^\w]+/g, '-').replace(/(^-|-$)/g, '');
|
||||||
|
headings.push({ level: m[1].length, text, id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return headings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Inject id attributes into rendered HTML headings ─────────────────
|
||||||
|
|
||||||
|
function injectHeadingIds(html, headings) {
|
||||||
|
let idx = 0;
|
||||||
|
return html.replace(/<(h[1-6])>/gi, (match, tag) => {
|
||||||
|
if (idx < headings.length) {
|
||||||
|
return `<${tag} id="${headings[idx++].id}">`;
|
||||||
|
}
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Build a tree from flat heading list ──────────────────────────────
|
||||||
|
|
||||||
|
function buildTocTree(headings) {
|
||||||
|
const root = { children: [] };
|
||||||
|
const stack = [{ node: root, level: 0 }];
|
||||||
|
for (const h of headings) {
|
||||||
|
const item = { ...h, children: [] };
|
||||||
|
while (stack.length > 1 && stack[stack.length - 1].level >= h.level) stack.pop();
|
||||||
|
stack[stack.length - 1].node.children.push(item);
|
||||||
|
stack.push({ node: item, level: h.level });
|
||||||
|
}
|
||||||
|
return root.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── TOC sidebar component ────────────────────────────────────────────
|
||||||
|
|
||||||
|
function TocItem({ item, collapsed, onToggle, onNavigate }) {
|
||||||
|
const hasChildren = item.children.length > 0;
|
||||||
|
const isCollapsed = collapsed[item.id];
|
||||||
|
return (
|
||||||
|
<li className="help-toc-item">
|
||||||
|
<div className="help-toc-row">
|
||||||
|
{hasChildren ? (
|
||||||
|
<button
|
||||||
|
className="help-toc-arrow"
|
||||||
|
onClick={(e) => { e.stopPropagation(); onToggle(item.id); }}
|
||||||
|
>
|
||||||
|
{isCollapsed ? '▶' : '▼'}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span className="help-toc-arrow-spacer" />
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
className="help-toc-link"
|
||||||
|
href={`#${item.id}`}
|
||||||
|
onClick={(e) => { e.preventDefault(); onNavigate(item.id); }}
|
||||||
|
>
|
||||||
|
{item.text}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{hasChildren && !isCollapsed && (
|
||||||
|
<ul className="help-toc-list">
|
||||||
|
{item.children.map((child) => (
|
||||||
|
<TocItem key={child.id} item={child} collapsed={collapsed} onToggle={onToggle} onNavigate={onNavigate} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Toc({ headings, contentRef }) {
|
||||||
|
const [collapsed, setCollapsed] = useState({});
|
||||||
|
const tree = useMemo(() => buildTocTree(headings), [headings]);
|
||||||
|
|
||||||
|
const onToggle = (id) => setCollapsed((prev) => ({ ...prev, [id]: !prev[id] }));
|
||||||
|
|
||||||
|
const onNavigate = (id) => {
|
||||||
|
const el = contentRef.current?.querySelector(`#${CSS.escape(id)}`);
|
||||||
|
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tree.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="help-toc nowheel">
|
||||||
|
<ul className="help-toc-list help-toc-root">
|
||||||
|
{tree.map((item) => (
|
||||||
|
<TocItem key={item.id} item={item} collapsed={collapsed} onToggle={onToggle} onNavigate={onNavigate} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Click handler for .md links ──────────────────────────────────────
|
||||||
|
|
||||||
|
function useMdLinkHandler(onOpenDoc) {
|
||||||
|
return (e) => {
|
||||||
|
const a = e.target.closest('a[href]');
|
||||||
|
if (!a) return;
|
||||||
|
const href = a.getAttribute('href');
|
||||||
|
if (href && /\.md$/i.test(href) && !href.startsWith('http')) {
|
||||||
|
e.preventDefault();
|
||||||
|
const filename = href.split('/').pop();
|
||||||
|
onOpenDoc(filename);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Content pane with TOC ────────────────────────────────────────────
|
||||||
|
|
||||||
|
function HelpContent({ content, onOpenDoc }) {
|
||||||
|
const contentRef = useRef(null);
|
||||||
|
const handleClick = useMdLinkHandler(onOpenDoc);
|
||||||
|
const md = content || '*Loading…*';
|
||||||
|
const headings = useMemo(() => parseHeadings(md), [md]);
|
||||||
|
const html = useMemo(() => {
|
||||||
|
let rendered;
|
||||||
|
try { rendered = marked.parse(md); } catch { rendered = md; }
|
||||||
|
return injectHeadingIds(rendered, headings);
|
||||||
|
}, [md, headings]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="help-content-row">
|
||||||
|
<Toc headings={headings} contentRef={contentRef} />
|
||||||
|
<div
|
||||||
|
ref={contentRef}
|
||||||
|
className="node-help-panel-body nowheel"
|
||||||
|
onClick={handleClick}
|
||||||
|
// eslint-disable-next-line react/no-danger
|
||||||
|
dangerouslySetInnerHTML={{ __html: html }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Journal tab ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function JournalTab({ content, onChange, onOpenDoc }) {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const contentRef = useRef(null);
|
||||||
|
const handleClick = useMdLinkHandler(onOpenDoc);
|
||||||
|
|
||||||
let renderedHtml = '';
|
let renderedHtml = '';
|
||||||
|
let headings = [];
|
||||||
if (!isEditing && content?.trim()) {
|
if (!isEditing && content?.trim()) {
|
||||||
try { renderedHtml = marked.parse(content); } catch { /* fallback to raw */ renderedHtml = content; }
|
headings = parseHeadings(content);
|
||||||
|
try { renderedHtml = injectHeadingIds(marked.parse(content), headings); } catch { renderedHtml = content; }
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -39,12 +191,17 @@ function JournalTab({ content, onChange }) {
|
|||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
) : renderedHtml ? (
|
) : renderedHtml ? (
|
||||||
<div
|
<div className="help-content-row">
|
||||||
className="node-help-panel-body node-help-journal-preview nowheel"
|
<Toc headings={headings} contentRef={contentRef} />
|
||||||
onDoubleClick={() => setIsEditing(true)}
|
<div
|
||||||
// eslint-disable-next-line react/no-danger
|
ref={contentRef}
|
||||||
dangerouslySetInnerHTML={{ __html: renderedHtml }}
|
className="node-help-panel-body node-help-journal-preview nowheel"
|
||||||
/>
|
onDoubleClick={() => setIsEditing(true)}
|
||||||
|
onClick={handleClick}
|
||||||
|
// eslint-disable-next-line react/no-danger
|
||||||
|
dangerouslySetInnerHTML={{ __html: renderedHtml }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="node-help-panel-body node-help-journal-preview nowheel"
|
className="node-help-panel-body node-help-journal-preview nowheel"
|
||||||
@@ -57,7 +214,9 @@ function JournalTab({ content, onChange }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function HelpPanelManager({ tabs, activeTab, onTabSelect, onTabClose, onTabContentChange, onOpenJournal }) {
|
// ── Main panel manager ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
function HelpPanelManager({ tabs, activeTab, onTabSelect, onTabClose, onTabContentChange, onOpenJournal, onOpenDoc }) {
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -116,13 +275,10 @@ function HelpPanelManager({ tabs, activeTab, onTabSelect, onTabClose, onTabConte
|
|||||||
<JournalTab
|
<JournalTab
|
||||||
content={active.content}
|
content={active.content}
|
||||||
onChange={(val) => onTabContentChange(active.label, val)}
|
onChange={(val) => onTabContentChange(active.label, val)}
|
||||||
|
onOpenDoc={onOpenDoc}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<HelpContent content={active.content} onOpenDoc={onOpenDoc} />
|
||||||
className="node-help-panel-body nowheel"
|
|
||||||
// eslint-disable-next-line react/no-danger
|
|
||||||
dangerouslySetInnerHTML={{ __html: marked.parse(active.content || '*Loading…*') }}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>,
|
</div>,
|
||||||
|
|||||||
@@ -566,7 +566,7 @@ html, body, #root {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
top: 60px;
|
top: 60px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
width: 420px;
|
width: 620px;
|
||||||
max-height: calc(100vh - 80px);
|
max-height: calc(100vh - 80px);
|
||||||
background: #1e293b;
|
background: #1e293b;
|
||||||
border: 1px solid #334155;
|
border: 1px solid #334155;
|
||||||
@@ -700,6 +700,82 @@ html, body, #root {
|
|||||||
|
|
||||||
.node-help-panel-body strong { color: #e2e8f0; }
|
.node-help-panel-body strong { color: #e2e8f0; }
|
||||||
|
|
||||||
|
/* ── Help panel TOC + content layout ──────────────────────────────── */
|
||||||
|
|
||||||
|
.help-content-row {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-content-row > .node-help-panel-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-toc {
|
||||||
|
width: 160px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-right: 1px solid #1e293b;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 11px;
|
||||||
|
background: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-toc-root { padding: 0; }
|
||||||
|
|
||||||
|
.help-toc-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-toc-item {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-toc-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-toc-arrow {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 7px;
|
||||||
|
padding: 0;
|
||||||
|
width: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1;
|
||||||
|
transition: color 0.12s;
|
||||||
|
}
|
||||||
|
.help-toc-arrow:hover { color: #94a3b8; }
|
||||||
|
|
||||||
|
.help-toc-arrow-spacer {
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-toc-link {
|
||||||
|
color: #94a3b8;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 2px 6px 2px 0;
|
||||||
|
display: block;
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: color 0.12s;
|
||||||
|
}
|
||||||
|
.help-toc-link:hover { color: #f1f5f9; }
|
||||||
|
|
||||||
.node-body {
|
.node-body {
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export default defineConfig({
|
|||||||
'/upload-folder': 'http://127.0.0.1:8188',
|
'/upload-folder': 'http://127.0.0.1:8188',
|
||||||
'/upload': 'http://127.0.0.1:8188',
|
'/upload': 'http://127.0.0.1:8188',
|
||||||
'/download': 'http://127.0.0.1:8188',
|
'/download': 'http://127.0.0.1:8188',
|
||||||
|
'/help-docs': { target: 'http://127.0.0.1:8188', changeOrigin: true },
|
||||||
'/prompt': 'http://127.0.0.1:8188',
|
'/prompt': 'http://127.0.0.1:8188',
|
||||||
'/ws': {
|
'/ws': {
|
||||||
target: 'http://127.0.0.1:8188',
|
target: 'http://127.0.0.1:8188',
|
||||||
|
|||||||
Reference in New Issue
Block a user