feat(web): add tools approvals models and images
This commit is contained in:
+67
-11
@@ -1,15 +1,26 @@
|
||||
import { AssistantRuntimeProvider } from "@assistant-ui/react";
|
||||
import { useEffect, useState, useSyncExternalStore } from "react";
|
||||
import type { ImageAttachment, ModelConnectionInput } from "@agent-studio/shared";
|
||||
import { HarnessAttachmentAdapter } from "./assistant/attachment-adapter.js";
|
||||
import { useHarnessRuntime, type HarnessRuntimeStore } from "./assistant/use-harness-runtime.js";
|
||||
import { Thread } from "./components/chat/Thread.js";
|
||||
import { AppShell } from "./components/layout/AppShell.js";
|
||||
import { SkillsPanel } from "./components/skills/SkillsPanel.js";
|
||||
import { EventTimeline } from "./components/timeline/EventTimeline.js";
|
||||
import { ApprovalDialog } from "./components/approvals/ApprovalDialog.js";
|
||||
import { ModelSettings, type ModelView } from "./components/settings/ModelSettings.js";
|
||||
import { HarnessClientStore } from "./store/harness-client-store.js";
|
||||
|
||||
type AppStore = HarnessRuntimeStore & {
|
||||
start(): Promise<void>;
|
||||
getModelConfig(): Promise<{ modelConfig: { configured: boolean; modelId?: string; supportsImages?: boolean } }>;
|
||||
getModelConfig(): Promise<{ modelConfig: ModelView }>;
|
||||
putModelConfig?: (input: ModelConnectionInput) => Promise<{ modelConfig: ModelView }>;
|
||||
testModelConfig?: () => Promise<{ status: "ok" | "deferred"; message: string }>;
|
||||
clearCredentials?: () => Promise<unknown>;
|
||||
approve?: (approvalId: string) => Promise<unknown>;
|
||||
deny?: (approvalId: string) => Promise<unknown>;
|
||||
uploadAttachment?: (file: File) => Promise<{ attachment: ImageAttachment }>;
|
||||
deleteAttachment?: (attachmentId: string) => Promise<unknown>;
|
||||
};
|
||||
|
||||
export type AppProps = {
|
||||
@@ -21,43 +32,56 @@ export type AppProps = {
|
||||
export function App({ store: suppliedStore, autoStart = true, modelConfigured: suppliedModelConfigured }: AppProps) {
|
||||
const [ownedStore] = useState(() => new HarnessClientStore());
|
||||
const store = suppliedStore ?? ownedStore;
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const snapshot = useSyncExternalStore(
|
||||
(subscriber) => store.subscribe(subscriber),
|
||||
() => store.getSnapshot(),
|
||||
() => store.getSnapshot(),
|
||||
);
|
||||
const [model, setModel] = useState({
|
||||
const [model, setModel] = useState<ModelView>({
|
||||
configured: suppliedModelConfigured ?? false,
|
||||
source: suppliedModelConfigured === true ? "session" : "missing",
|
||||
modelId: suppliedModelConfigured === true ? "Configured model" : "No model",
|
||||
supportsImages: false,
|
||||
});
|
||||
const runtime = useHarnessRuntime(store, { isSendDisabled: !model.configured });
|
||||
const [attachmentAdapter] = useState(() => {
|
||||
return store.uploadAttachment === undefined || store.deleteAttachment === undefined
|
||||
? undefined
|
||||
: new HarnessAttachmentAdapter({
|
||||
uploadAttachment: (file) => store.uploadAttachment?.(file) ?? Promise.reject(new Error("附件上传接口不可用")),
|
||||
deleteAttachment: (attachmentId) => store.deleteAttachment?.(attachmentId) ?? Promise.reject(new Error("附件删除接口不可用")),
|
||||
});
|
||||
});
|
||||
const activeAttachmentAdapter = model.supportsImages === true ? attachmentAdapter : undefined;
|
||||
const runtime = useHarnessRuntime(store, {
|
||||
isSendDisabled: !model.configured,
|
||||
...(activeAttachmentAdapter === undefined ? {} : { attachmentAdapter: activeAttachmentAdapter }),
|
||||
});
|
||||
const busy = Object.values(snapshot.runs).some((run) => run.status === "running");
|
||||
const approvalPending = Object.values(snapshot.approvals).some((approval) => approval.status === "pending");
|
||||
const pendingApproval = Object.values(snapshot.approvals).find((approval) => approval.status === "pending");
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoStart) return undefined;
|
||||
let active = true;
|
||||
void store.start().then(async () => {
|
||||
const response = await store.getModelConfig();
|
||||
if (active) setModel({
|
||||
configured: response.modelConfig.configured,
|
||||
modelId: response.modelConfig.modelId ?? "No model",
|
||||
supportsImages: response.modelConfig.supportsImages ?? false,
|
||||
});
|
||||
if (active) setModel(response.modelConfig);
|
||||
}).catch(() => undefined);
|
||||
return () => { active = false; };
|
||||
}, [autoStart, store]);
|
||||
|
||||
useEffect(() => () => attachmentAdapter?.dispose(), [attachmentAdapter]);
|
||||
|
||||
return (
|
||||
<AssistantRuntimeProvider runtime={runtime}>
|
||||
<AppShell
|
||||
topBar={{
|
||||
...(snapshot.sessionId === undefined ? {} : { sessionId: snapshot.sessionId }),
|
||||
connection: snapshot.connection,
|
||||
modelLabel: model.modelId,
|
||||
supportsImages: model.supportsImages,
|
||||
onOpenSettings: () => { window.dispatchEvent(new CustomEvent("agent-studio:open-settings")); },
|
||||
modelLabel: model.modelId ?? "No model",
|
||||
supportsImages: model.supportsImages ?? false,
|
||||
onOpenSettings: () => setSettingsOpen(true),
|
||||
onReset: () => window.location.reload(),
|
||||
}}
|
||||
skills={<SkillsPanel skills={snapshot.skills} />}
|
||||
@@ -68,9 +92,41 @@ export function App({ store: suppliedStore, autoStart = true, modelConfigured: s
|
||||
connection={snapshot.connection}
|
||||
error={snapshot.error}
|
||||
approvalPending={approvalPending}
|
||||
attachmentAdapter={activeAttachmentAdapter}
|
||||
/>}
|
||||
timeline={<EventTimeline events={snapshot.timeline} />}
|
||||
/>
|
||||
{pendingApproval !== undefined ? (
|
||||
<ApprovalDialog
|
||||
request={pendingApproval}
|
||||
onApprove={(approvalId) => store.approve?.(approvalId) ?? Promise.reject(new Error("Approval API unavailable"))}
|
||||
onDeny={(approvalId) => store.deny?.(approvalId) ?? Promise.reject(new Error("Approval API unavailable"))}
|
||||
/>
|
||||
) : null}
|
||||
<ModelSettings
|
||||
open={settingsOpen}
|
||||
busy={busy}
|
||||
current={model}
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
onSave={async (input) => {
|
||||
if (store.putModelConfig === undefined) throw new Error("模型配置接口不可用");
|
||||
const result = await store.putModelConfig(input);
|
||||
setModel(result.modelConfig);
|
||||
return result.modelConfig;
|
||||
}}
|
||||
onTest={async () => {
|
||||
if (store.testModelConfig === undefined) throw new Error("连接测试接口不可用");
|
||||
return store.testModelConfig();
|
||||
}}
|
||||
onClear={async () => {
|
||||
if (store.clearCredentials !== undefined) await store.clearCredentials();
|
||||
setModel((current) => {
|
||||
const next = { ...current, configured: false, source: "missing" as const };
|
||||
delete next.keyHint;
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</AssistantRuntimeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user