feat: merge opus comfyui and login updates
This commit is contained in:
+216
-10
@@ -1,6 +1,8 @@
|
||||
import { useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
|
||||
import { getAuthToken } from '../authSession.js';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { getSessionToken } from '../authSession.js';
|
||||
import { useToast } from '../components/Toast.js';
|
||||
import { filterModelTesterModelNames } from './helpers/modelTesterSession.js';
|
||||
|
||||
type AgentRole = 'user' | 'assistant' | 'system';
|
||||
|
||||
@@ -54,6 +56,11 @@ type StreamAccumulator = {
|
||||
workflowJson: unknown;
|
||||
};
|
||||
|
||||
type StoredAgentSession = {
|
||||
messages?: AgentMessage[];
|
||||
selectedModel?: string;
|
||||
};
|
||||
|
||||
const WELCOME_MESSAGE: AgentMessage = {
|
||||
id: 'welcome',
|
||||
role: 'system',
|
||||
@@ -71,6 +78,53 @@ function createId(prefix: string) {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
}
|
||||
|
||||
function sessionStorageKey(audience: 'admin' | 'user') {
|
||||
return `boosapi.comfyuiAgent.${audience}.session`;
|
||||
}
|
||||
|
||||
function isValidMessage(value: unknown): value is AgentMessage {
|
||||
if (!value || typeof value !== 'object') return false;
|
||||
const item = value as Partial<AgentMessage>;
|
||||
return (
|
||||
typeof item.id === 'string' &&
|
||||
(item.role === 'user' || item.role === 'assistant' || item.role === 'system') &&
|
||||
typeof item.content === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
function readStoredSession(audience: 'admin' | 'user'): StoredAgentSession {
|
||||
try {
|
||||
const raw = localStorage.getItem(sessionStorageKey(audience));
|
||||
if (!raw) return {};
|
||||
const parsed = JSON.parse(raw) as StoredAgentSession;
|
||||
const messages = Array.isArray(parsed.messages)
|
||||
? parsed.messages.filter(isValidMessage)
|
||||
: undefined;
|
||||
return {
|
||||
messages: messages?.length ? messages : undefined,
|
||||
selectedModel:
|
||||
typeof parsed.selectedModel === 'string' ? parsed.selectedModel : undefined,
|
||||
};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function persistStoredSession(
|
||||
audience: 'admin' | 'user',
|
||||
messages: AgentMessage[],
|
||||
selectedModel: string,
|
||||
) {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
sessionStorageKey(audience),
|
||||
JSON.stringify({ messages, selectedModel }),
|
||||
);
|
||||
} catch {
|
||||
// Ignore quota/private-mode failures; chat still works for the current page.
|
||||
}
|
||||
}
|
||||
|
||||
function angleLabel(angle?: string) {
|
||||
if (angle === 'front') return '正面';
|
||||
if (angle === 'back') return '背面';
|
||||
@@ -124,11 +178,25 @@ async function readSseStream(
|
||||
|
||||
export default function ComfyUIAgent() {
|
||||
const toast = useToast();
|
||||
const [messages, setMessages] = useState<AgentMessage[]>([WELCOME_MESSAGE]);
|
||||
const location = useLocation();
|
||||
const sessionAudience = location.pathname.startsWith('/user/') ? 'user' : 'admin';
|
||||
const initialStoredSession = useMemo(
|
||||
() => readStoredSession(sessionAudience),
|
||||
[sessionAudience],
|
||||
);
|
||||
const [messages, setMessages] = useState<AgentMessage[]>(
|
||||
() => initialStoredSession.messages || [WELCOME_MESSAGE],
|
||||
);
|
||||
const [input, setInput] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [streaming, setStreaming] = useState<StreamAccumulator | null>(null);
|
||||
const [status, setStatus] = useState<AgentStatus | null>(null);
|
||||
const [modelOptions, setModelOptions] = useState<string[]>([]);
|
||||
const [selectedModel, setSelectedModel] = useState(
|
||||
() => initialStoredSession.selectedModel || '',
|
||||
);
|
||||
const [modelSearch, setModelSearch] = useState('');
|
||||
const [loadingModels, setLoadingModels] = useState(false);
|
||||
const chatEndRef = useRef<HTMLDivElement>(null);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
@@ -139,13 +207,38 @@ export default function ComfyUIAgent() {
|
||||
}
|
||||
return streaming?.workflowJson || null;
|
||||
}, [messages, streaming]);
|
||||
const filteredModelOptions = useMemo(
|
||||
() => {
|
||||
const filtered = filterModelTesterModelNames(modelOptions, modelSearch).slice(0, 80);
|
||||
if (selectedModel && !filtered.includes(selectedModel)) {
|
||||
return [selectedModel, ...filtered];
|
||||
}
|
||||
return filtered;
|
||||
},
|
||||
[modelOptions, modelSearch, selectedModel],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
chatEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
||||
}, [messages, streaming?.content]);
|
||||
|
||||
useEffect(() => {
|
||||
const token = getAuthToken(localStorage);
|
||||
const stored = readStoredSession(sessionAudience);
|
||||
setMessages(stored.messages || [WELCOME_MESSAGE]);
|
||||
setSelectedModel(stored.selectedModel || '');
|
||||
setModelSearch('');
|
||||
setStreaming(null);
|
||||
setLoading(false);
|
||||
abortRef.current?.abort();
|
||||
abortRef.current = null;
|
||||
}, [sessionAudience]);
|
||||
|
||||
useEffect(() => {
|
||||
persistStoredSession(sessionAudience, messages, selectedModel);
|
||||
}, [messages, selectedModel, sessionAudience]);
|
||||
|
||||
useEffect(() => {
|
||||
const token = getSessionToken(sessionAudience, localStorage);
|
||||
if (!token) return;
|
||||
void fetch('/api/comfyui-agent/status', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
@@ -153,7 +246,40 @@ export default function ComfyUIAgent() {
|
||||
.then((response) => (response.ok ? response.json() : null))
|
||||
.then((payload) => setStatus(payload || null))
|
||||
.catch(() => setStatus(null));
|
||||
}, []);
|
||||
}, [sessionAudience]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadRouteModels = async () => {
|
||||
const token = getSessionToken(sessionAudience, localStorage);
|
||||
if (!token) return;
|
||||
setLoadingModels(true);
|
||||
try {
|
||||
const response = await fetch('/api/comfyui-agent/models', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const payload = await response.json();
|
||||
const names = Array.isArray(payload?.models)
|
||||
? payload.models
|
||||
.map((model: unknown) => (typeof model === 'string' ? model.trim() : ''))
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
setModelOptions(names);
|
||||
setSelectedModel((current) => {
|
||||
if (current && names.includes(current)) return current;
|
||||
const configuredModel = String(payload?.defaultModel || status?.model || '').trim();
|
||||
if (configuredModel && names.includes(configuredModel)) return configuredModel;
|
||||
return names[0] || configuredModel;
|
||||
});
|
||||
} catch {
|
||||
toast.error('加载模型路由失败');
|
||||
} finally {
|
||||
setLoadingModels(false);
|
||||
}
|
||||
};
|
||||
|
||||
void loadRouteModels();
|
||||
}, [sessionAudience, status?.model, toast]);
|
||||
|
||||
const exportWorkflow = () => {
|
||||
const workflow = latestWorkflow || {
|
||||
@@ -250,7 +376,7 @@ export default function ComfyUIAgent() {
|
||||
const content = input.trim();
|
||||
if (!content || loading) return;
|
||||
|
||||
const token = getAuthToken(localStorage);
|
||||
const token = getSessionToken(sessionAudience, localStorage);
|
||||
if (!token) {
|
||||
toast.error('登录状态已过期,请重新登录');
|
||||
return;
|
||||
@@ -291,7 +417,10 @@ export default function ComfyUIAgent() {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ messages: payloadMessages }),
|
||||
body: JSON.stringify({
|
||||
messages: payloadMessages,
|
||||
model: selectedModel || status?.model,
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
@@ -395,12 +524,45 @@ export default function ComfyUIAgent() {
|
||||
<div>
|
||||
<h1 style={titleStyle}>视频助手</h1>
|
||||
<div style={statusStyle}>
|
||||
模型 {status?.model || '加载中'} · 上游 {status?.upstreamBase || '检测中'} · ComfyUI {status?.comfyui?.status || 'unknown'}
|
||||
模型 {selectedModel || status?.model || '加载中'} · 上游 {status?.upstreamBase || '检测中'} · ComfyUI {status?.comfyui?.status || 'unknown'}
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onClick={exportWorkflow} style={secondaryButtonStyle}>
|
||||
导出工作流
|
||||
</button>
|
||||
<div style={headerActionsStyle}>
|
||||
<label style={modelPickerStyle}>
|
||||
<span style={modelPickerLabelStyle}>对话模型</span>
|
||||
<input
|
||||
value={modelSearch}
|
||||
onChange={(event) => setModelSearch(event.target.value)}
|
||||
placeholder={loadingModels ? '加载模型...' : '搜索模型路由'}
|
||||
disabled={loading || loadingModels || modelOptions.length === 0}
|
||||
style={modelSearchInputStyle}
|
||||
/>
|
||||
<select
|
||||
value={selectedModel}
|
||||
onChange={(event) => {
|
||||
setSelectedModel(event.target.value);
|
||||
setModelSearch('');
|
||||
}}
|
||||
disabled={loading || loadingModels || modelOptions.length === 0}
|
||||
style={modelSelectStyle}
|
||||
>
|
||||
{modelOptions.length === 0 ? (
|
||||
<option value={status?.model || ''}>
|
||||
{loadingModels ? '模型加载中' : (status?.model || '暂无模型路由')}
|
||||
</option>
|
||||
) : filteredModelOptions.length === 0 ? (
|
||||
<option value={selectedModel}>没有匹配模型</option>
|
||||
) : (
|
||||
filteredModelOptions.map((model) => (
|
||||
<option key={model} value={model}>{model}</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" onClick={exportWorkflow} style={secondaryButtonStyle}>
|
||||
导出工作流
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section style={quickPromptStyle}>
|
||||
@@ -506,6 +668,14 @@ const headerStyle: CSSProperties = {
|
||||
background: 'var(--color-surface)',
|
||||
};
|
||||
|
||||
const headerActionsStyle: CSSProperties = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 10,
|
||||
flexWrap: 'wrap',
|
||||
};
|
||||
|
||||
const titleStyle: CSSProperties = {
|
||||
margin: 0,
|
||||
fontSize: 18,
|
||||
@@ -518,6 +688,42 @@ const statusStyle: CSSProperties = {
|
||||
color: 'var(--color-text-muted)',
|
||||
};
|
||||
|
||||
const modelPickerStyle: CSSProperties = {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'minmax(130px, 180px) minmax(180px, 260px)',
|
||||
gap: 8,
|
||||
alignItems: 'end',
|
||||
};
|
||||
|
||||
const modelPickerLabelStyle: CSSProperties = {
|
||||
gridColumn: '1 / -1',
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
color: 'var(--color-text-muted)',
|
||||
};
|
||||
|
||||
const modelSearchInputStyle: CSSProperties = {
|
||||
minHeight: 36,
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 8,
|
||||
padding: '7px 10px',
|
||||
background: 'var(--color-bg)',
|
||||
color: 'var(--color-text-primary)',
|
||||
fontSize: 12,
|
||||
outline: 'none',
|
||||
};
|
||||
|
||||
const modelSelectStyle: CSSProperties = {
|
||||
minHeight: 36,
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 8,
|
||||
padding: '7px 10px',
|
||||
background: 'var(--color-bg)',
|
||||
color: 'var(--color-text-primary)',
|
||||
fontSize: 12,
|
||||
outline: 'none',
|
||||
};
|
||||
|
||||
const quickPromptStyle: CSSProperties = {
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
|
||||
Reference in New Issue
Block a user