Refine panel assistants and observability routing
This commit is contained in:
parent
2cb3f4ee88
commit
bfd9a9ca63
@ -9,7 +9,7 @@ import {
|
||||
Rocket,
|
||||
Database,
|
||||
Key,
|
||||
History,
|
||||
Activity,
|
||||
Settings,
|
||||
Plus,
|
||||
} from 'lucide-react'
|
||||
@ -20,7 +20,7 @@ const navItems = [
|
||||
{ href: '/deployments', label: 'Deployments', icon: Rocket },
|
||||
{ href: '/resources', label: 'Resources', icon: Database },
|
||||
{ href: '/api-keys', label: 'API Keys', icon: Key },
|
||||
{ href: '/logs', label: 'Logs', icon: History },
|
||||
{ href: '/panel/observability', label: 'Observability', icon: Activity },
|
||||
{ href: '/settings', label: 'Settings', icon: Settings },
|
||||
]
|
||||
|
||||
|
||||
@ -50,6 +50,9 @@ type OpenClawAssistantPaneProps = {
|
||||
initialQuestion?: string;
|
||||
initialQuestionKey?: number;
|
||||
variant?: "page" | "sidebar";
|
||||
showConversation?: boolean;
|
||||
emptyConversationHint?: string;
|
||||
onStateChange?: (state: OpenClawAssistantViewState) => void;
|
||||
};
|
||||
|
||||
type ComposerAttachment = GatewayChatAttachmentPayload & {
|
||||
@ -60,6 +63,23 @@ type ComposerAttachment = GatewayChatAttachmentPayload & {
|
||||
|
||||
type ConnectionState = "idle" | "connecting" | "ready" | "error";
|
||||
|
||||
export type OpenClawAssistantViewState = {
|
||||
connectionState: ConnectionState;
|
||||
healthBadge: string;
|
||||
errorMessage: string;
|
||||
hasGateway: boolean;
|
||||
selectedSessionLabel: string;
|
||||
streamingText: string;
|
||||
streamingHtml: string;
|
||||
messages: Array<{
|
||||
id: string;
|
||||
role: string;
|
||||
text: string;
|
||||
html: string;
|
||||
timestampMs?: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
function pickCopy(isChinese: boolean, zh: string, en: string): string {
|
||||
return isChinese ? zh : en;
|
||||
}
|
||||
@ -195,6 +215,9 @@ export function OpenClawAssistantPane({
|
||||
initialQuestion,
|
||||
initialQuestionKey,
|
||||
variant = "page",
|
||||
showConversation = true,
|
||||
emptyConversationHint,
|
||||
onStateChange,
|
||||
}: OpenClawAssistantPaneProps) {
|
||||
const router = useRouter();
|
||||
const { language } = useLanguage();
|
||||
@ -432,6 +455,34 @@ export function OpenClawAssistantPane({
|
||||
[messages, minimalPage],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onStateChange?.({
|
||||
connectionState,
|
||||
healthBadge,
|
||||
errorMessage,
|
||||
hasGateway: Boolean(openclawUrl.trim()),
|
||||
selectedSessionLabel:
|
||||
activeSession?.derivedTitle ||
|
||||
activeSession?.displayName ||
|
||||
selectedSessionKey ||
|
||||
copy.mainSession,
|
||||
streamingText,
|
||||
streamingHtml: streamingText ? renderMarkdown(streamingText) : "",
|
||||
messages: renderedMessages,
|
||||
});
|
||||
}, [
|
||||
activeSession,
|
||||
connectionState,
|
||||
copy.mainSession,
|
||||
errorMessage,
|
||||
healthBadge,
|
||||
onStateChange,
|
||||
openclawUrl,
|
||||
renderedMessages,
|
||||
selectedSessionKey,
|
||||
streamingText,
|
||||
]);
|
||||
|
||||
const connectGateway = useCallback(
|
||||
async (nextSessionKey?: string, nextAgentId?: string): Promise<void> => {
|
||||
if (!openclawUrl.trim()) {
|
||||
@ -904,7 +955,26 @@ export function OpenClawAssistantPane({
|
||||
) : null}
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-3 py-3">
|
||||
{!openclawUrl.trim() ? (
|
||||
{!showConversation ? (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4 rounded-[var(--radius-xl)] border border-dashed border-[color:var(--color-surface-border)] bg-[var(--color-surface-muted)]/40 px-5 text-center">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-[var(--radius-lg)] bg-[var(--color-primary-muted)] text-[var(--color-primary)]">
|
||||
<Sparkles className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-base font-semibold text-[var(--color-heading)]">
|
||||
{copy.assistantTitle}
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--color-text-subtle)]">
|
||||
{emptyConversationHint ??
|
||||
pickCopy(
|
||||
isChinese,
|
||||
"在右侧发起任务,中间区域会同步展示助手结果。",
|
||||
"Start tasks from the right panel. Results will be mirrored in the center workspace.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : !openclawUrl.trim() ? (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 rounded-[var(--radius-xl)] border border-dashed border-[color:var(--color-surface-border)] bg-[var(--color-surface-muted)]/40 px-5 text-center">
|
||||
<Sparkles className="h-7 w-7 text-[var(--color-primary)]" />
|
||||
<div className="space-y-2">
|
||||
|
||||
@ -998,7 +998,7 @@ export const translations: Record<'en' | 'zh', Translation> = {
|
||||
deployments: 'Deployments',
|
||||
resources: 'Resources',
|
||||
apiKeys: 'API Keys',
|
||||
logs: 'Logs',
|
||||
logs: 'Observability',
|
||||
settings: 'Settings',
|
||||
},
|
||||
overview: {
|
||||
@ -1168,10 +1168,28 @@ export const translations: Record<'en' | 'zh', Translation> = {
|
||||
{
|
||||
title: 'Full-link SaaS Hosting',
|
||||
description: 'Provide one-stop hosting services from development and deployment to maintenance, simplifying architectural complexity and helping applications quickly achieve SaaS transformation.',
|
||||
guide: {
|
||||
title: 'Deployments Console Guide',
|
||||
dismiss: 'Exit Guide',
|
||||
steps: [
|
||||
{ text: 'Use the deployments console as the operational entry for release progress, blockers, and execution history.' },
|
||||
{ text: 'Open the Deployments workspace and let X Assistant generate the rollout checklist or summarize current status.', link: { url: '/panel/deployments', label: 'Open Deployments Console' } },
|
||||
{ text: 'Once connected, ask for deployment diagnosis, release steps, or rollback suggestions, and the result stream will appear in the center workspace.' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'AI-Driven Observability',
|
||||
description: 'Utilize AI to intelligently analyze full-link logs and performance metrics, identifying potential anomalies in real-time and providing predictive insights to ensure smooth system operation.',
|
||||
guide: {
|
||||
title: 'Observability Console Guide',
|
||||
dismiss: 'Exit Guide',
|
||||
steps: [
|
||||
{ text: 'Use the observability console as the AI entry for logs, metrics, and anomaly investigation.' },
|
||||
{ text: 'Open the Observability workspace and start from log analysis, timeline summaries, or incident review.', link: { url: '/panel/observability', label: 'Open Observability Console' } },
|
||||
{ text: 'X Assistant stays open on the right while the center pane continuously mirrors analysis results, remediation ideas, and summaries.' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
nextSteps: {
|
||||
@ -1778,7 +1796,7 @@ export const translations: Record<'en' | 'zh', Translation> = {
|
||||
deployments: '部署管理',
|
||||
resources: '资源列表',
|
||||
apiKeys: '接口密钥',
|
||||
logs: '运行日志',
|
||||
logs: '可观测性',
|
||||
settings: '系统设置',
|
||||
},
|
||||
overview: {
|
||||
@ -1944,10 +1962,28 @@ export const translations: Record<'en' | 'zh', Translation> = {
|
||||
{
|
||||
title: '全链路 SaaS 托管',
|
||||
description: '提供从开发、部署到维护的一站式托管服务,简化架构复杂度,助力应用快速实现 SaaS 化转型。',
|
||||
guide: {
|
||||
title: '部署控制台向导',
|
||||
dismiss: '退出向导',
|
||||
steps: [
|
||||
{ text: '把部署控制台作为发布进度、阻塞项和执行记录的统一操作入口。' },
|
||||
{ text: '打开 Deployments 工作区,让 X 助手为你生成部署检查清单,或者总结当前发布状态。', link: { url: '/panel/deployments', label: '打开 Deployments 控制台' } },
|
||||
{ text: '接入完成后,可以继续让助手分析失败部署、补充回滚步骤,并把结果同步展示在中间工作区。' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'AI 驱动的可观测性',
|
||||
description: '利用 AI 智能分析全链路日志与性能指标,实时识别潜在异常并提供预测性洞察,保障系统平稳运行。',
|
||||
guide: {
|
||||
title: '可观测性控制台向导',
|
||||
dismiss: '退出向导',
|
||||
steps: [
|
||||
{ text: '把可观测性控制台作为日志、指标与异常诊断的 AI 工作入口。' },
|
||||
{ text: '打开 Observability 工作区,从日志分析、时间线梳理或事件复盘开始。', link: { url: '/panel/observability', label: '打开 Observability 控制台' } },
|
||||
{ text: '右侧 X 助手保持打开,中间区域持续展示分析结果、修复建议与总结。' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
nextSteps: {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Database, History, Key, Rocket, Settings } from 'lucide-react'
|
||||
import { Activity, Database, Key, Rocket, Settings } from 'lucide-react'
|
||||
|
||||
import type { DashboardExtension } from '../../types'
|
||||
|
||||
@ -6,10 +6,10 @@ export const infraExtension: DashboardExtension = {
|
||||
id: 'builtin.infra',
|
||||
meta: {
|
||||
title: '基础设施管理',
|
||||
description: '云基础设施、部署、资源与日志管理。',
|
||||
description: '云基础设施、部署、资源与可观测性管理。',
|
||||
version: '1.0.0',
|
||||
author: 'Cloud-Neutral',
|
||||
keywords: ['infrastructure', 'deployments', 'resources', 'logs'],
|
||||
keywords: ['infrastructure', 'deployments', 'resources', 'observability'],
|
||||
},
|
||||
routes: [
|
||||
{
|
||||
@ -44,10 +44,10 @@ export const infraExtension: DashboardExtension = {
|
||||
},
|
||||
{
|
||||
id: 'logs',
|
||||
path: '/panel/logs',
|
||||
label: 'Logs',
|
||||
description: '系统流水与审计日志',
|
||||
icon: History,
|
||||
path: '/panel/observability',
|
||||
label: 'Observability',
|
||||
description: '监控、日志与 AI 分析',
|
||||
icon: Activity,
|
||||
loader: () => import('./routes/placeholder'),
|
||||
guard: { requireLogin: true },
|
||||
sidebar: { section: 'infra', order: 3 },
|
||||
|
||||
@ -1,22 +1,368 @@
|
||||
'use client'
|
||||
"use client";
|
||||
|
||||
import React from 'react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
Activity,
|
||||
ArrowRight,
|
||||
Database,
|
||||
KeyRound,
|
||||
Rocket,
|
||||
ShieldCheck,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
|
||||
import {
|
||||
OpenClawAssistantPane,
|
||||
type OpenClawAssistantViewState,
|
||||
} from "@/components/openclaw/OpenClawAssistantPane";
|
||||
import type { IntegrationDefaults } from "@/lib/openclaw/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type WorkspaceKind = "deployments" | "resources" | "logs" | "api-keys";
|
||||
|
||||
type WorkspaceConfig = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
icon: LucideIcon;
|
||||
accent: string;
|
||||
prompts: string[];
|
||||
suggestions: string[];
|
||||
};
|
||||
|
||||
const EMPTY_DEFAULTS: IntegrationDefaults = {
|
||||
openclawUrl: "",
|
||||
openclawOrigin: "",
|
||||
openclawTokenConfigured: false,
|
||||
vaultUrl: "",
|
||||
vaultNamespace: "",
|
||||
vaultTokenConfigured: false,
|
||||
vaultSecretPath: "",
|
||||
vaultSecretKey: "",
|
||||
apisixUrl: "",
|
||||
apisixTokenConfigured: false,
|
||||
};
|
||||
|
||||
function getWorkspaceKind(pathname: string): WorkspaceKind {
|
||||
if (pathname.includes("/resources")) {
|
||||
return "resources";
|
||||
}
|
||||
if (pathname.includes("/observability")) {
|
||||
return "logs";
|
||||
}
|
||||
if (pathname.includes("/api-keys")) {
|
||||
return "api-keys";
|
||||
}
|
||||
return "deployments";
|
||||
}
|
||||
|
||||
function getWorkspaceConfig(kind: WorkspaceKind): WorkspaceConfig {
|
||||
switch (kind) {
|
||||
case "resources":
|
||||
return {
|
||||
title: "Resources",
|
||||
subtitle: "让 X 助手整理资源盘点、实例状态和依赖关系,结果在这里持续展开。",
|
||||
icon: Database,
|
||||
accent: "R",
|
||||
prompts: [
|
||||
"盘点当前资源并按环境分组",
|
||||
"列出数据库实例与风险项",
|
||||
"整理资源依赖关系和下一步动作",
|
||||
],
|
||||
suggestions: [
|
||||
"资源清单",
|
||||
"实例状态",
|
||||
"风险摘要",
|
||||
],
|
||||
};
|
||||
case "logs":
|
||||
return {
|
||||
title: "Observability",
|
||||
subtitle: "把监控、日志与 AI 分析集中到同一个中间结果区,不再停留在空白页。",
|
||||
icon: Activity,
|
||||
accent: "O",
|
||||
prompts: [
|
||||
"分析最近异常日志并归类",
|
||||
"总结监控异常和修复建议",
|
||||
"按时间线梳理今天的可观测性事件",
|
||||
],
|
||||
suggestions: [
|
||||
"指标概览",
|
||||
"日志分析",
|
||||
"修复建议",
|
||||
],
|
||||
};
|
||||
case "api-keys":
|
||||
return {
|
||||
title: "API Keys",
|
||||
subtitle: "把接口密钥、访问凭证和安全引用整理成可交互的工作区结果。",
|
||||
icon: KeyRound,
|
||||
accent: "K",
|
||||
prompts: [
|
||||
"梳理当前密钥用途与系统归属",
|
||||
"列出需要轮换的访问凭证",
|
||||
"生成密钥治理检查清单",
|
||||
],
|
||||
suggestions: [
|
||||
"凭证盘点",
|
||||
"轮换建议",
|
||||
"治理清单",
|
||||
],
|
||||
};
|
||||
default:
|
||||
return {
|
||||
title: "Deployments",
|
||||
subtitle: "部署任务、发布状态和后续动作由 X 助手驱动,中间区直接展示结果流。",
|
||||
icon: Rocket,
|
||||
accent: "D",
|
||||
prompts: [
|
||||
"总结当前部署状态和阻塞项",
|
||||
"生成一次部署检查清单",
|
||||
"分析失败部署并给出修复步骤",
|
||||
],
|
||||
suggestions: [
|
||||
"部署状态",
|
||||
"阻塞项",
|
||||
"执行步骤",
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function StatusPill({
|
||||
label,
|
||||
tone = "neutral",
|
||||
}: {
|
||||
label: string;
|
||||
tone?: "neutral" | "success" | "danger";
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm font-semibold",
|
||||
tone === "success"
|
||||
? "border-emerald-200 bg-emerald-50 text-emerald-700"
|
||||
: tone === "danger"
|
||||
? "border-rose-200 bg-rose-50 text-rose-700"
|
||||
: "border-[color:var(--color-surface-border)] bg-white text-[var(--color-text-subtle)]",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"h-2.5 w-2.5 rounded-full",
|
||||
tone === "success"
|
||||
? "bg-emerald-500"
|
||||
: tone === "danger"
|
||||
? "bg-rose-500"
|
||||
: "bg-[var(--color-primary)]",
|
||||
)}
|
||||
/>
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PlaceholderPage() {
|
||||
const pathname = usePathname()
|
||||
const title = pathname.split('/').pop()?.replace(/-/g, ' ') || 'Page'
|
||||
const pathname = usePathname();
|
||||
const kind = useMemo(() => getWorkspaceKind(pathname), [pathname]);
|
||||
const config = useMemo(() => getWorkspaceConfig(kind), [kind]);
|
||||
const Icon = config.icon;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[60vh] text-center p-8">
|
||||
<div className="size-16 bg-primary/10 rounded-2xl flex items-center justify-center text-primary mb-6">
|
||||
<span className="text-2xl font-bold uppercase">{title.charAt(0)}</span>
|
||||
const [defaults, setDefaults] = useState<IntegrationDefaults>(EMPTY_DEFAULTS);
|
||||
const [promptSeed, setPromptSeed] = useState("");
|
||||
const [promptKey, setPromptKey] = useState(0);
|
||||
const [assistantState, setAssistantState] =
|
||||
useState<OpenClawAssistantViewState>({
|
||||
connectionState: "idle",
|
||||
healthBadge: "offline",
|
||||
errorMessage: "",
|
||||
hasGateway: false,
|
||||
selectedSessionLabel: "main",
|
||||
streamingText: "",
|
||||
streamingHtml: "",
|
||||
messages: [],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const response = await fetch("/api/integrations/defaults", {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
const payload = (await response.json()) as IntegrationDefaults;
|
||||
if (!cancelled) {
|
||||
setDefaults(payload);
|
||||
}
|
||||
} catch {
|
||||
// Keep empty defaults so the assistant can still render setup guidance.
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const assistantMessages = assistantState.messages.filter(
|
||||
(message) => message.role === "user" || message.role === "assistant",
|
||||
);
|
||||
const statusTone =
|
||||
assistantState.connectionState === "ready"
|
||||
? "success"
|
||||
: assistantState.connectionState === "error"
|
||||
? "danger"
|
||||
: "neutral";
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-[calc(100vh-11rem)] flex-col gap-5">
|
||||
<div className="grid min-h-0 flex-1 gap-5 xl:grid-cols-[minmax(0,1fr)_420px]">
|
||||
<section className="flex min-h-0 flex-col rounded-[26px] border border-[color:var(--color-surface-border)] bg-[var(--color-surface-elevated)] shadow-[var(--shadow-md)]">
|
||||
<div className="border-b border-[color:var(--color-surface-border)] px-6 py-5">
|
||||
<div className="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
|
||||
<div>
|
||||
<div className="mb-4 inline-flex h-14 w-14 items-center justify-center rounded-[18px] bg-[var(--color-primary-muted)] text-[var(--color-primary)]">
|
||||
<Icon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="text-5xl font-bold tracking-[-0.04em] text-[var(--color-primary)]">
|
||||
{config.accent}
|
||||
</div>
|
||||
<h1 className="mt-4 text-4xl font-semibold tracking-[-0.04em] text-[var(--color-heading)]">
|
||||
{config.title}
|
||||
</h1>
|
||||
<p className="mt-3 max-w-3xl text-lg leading-8 text-[var(--color-text-subtle)]">
|
||||
{config.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<StatusPill label={assistantState.healthBadge} tone={statusTone} />
|
||||
<StatusPill label={`Session · ${assistantState.selectedSessionLabel}`} />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-heading mb-3 capitalize">{title}</h1>
|
||||
<p className="text-text-muted max-w-md">
|
||||
This feature is currently under active development.
|
||||
It will soon provide a powerful interface for managing your {title.toLowerCase()}.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
<div className="mt-5 flex flex-wrap gap-3">
|
||||
{config.prompts.map((prompt) => (
|
||||
<button
|
||||
key={prompt}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setPromptSeed(prompt);
|
||||
setPromptKey((current) => current + 1);
|
||||
}}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-[color:var(--color-surface-border)] bg-white px-4 py-2 text-sm font-semibold text-[var(--color-text)] transition hover:border-[color:var(--color-primary-border)] hover:text-[var(--color-primary)]"
|
||||
>
|
||||
<Sparkles className="h-4 w-4" />
|
||||
{prompt}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-6 py-5">
|
||||
{!assistantState.hasGateway ? (
|
||||
<div className="flex h-full min-h-[420px] flex-col items-center justify-center rounded-[24px] border border-dashed border-[color:var(--color-surface-border)] bg-[var(--color-surface-muted)]/40 px-6 text-center">
|
||||
<ShieldCheck className="mb-4 h-10 w-10 text-[var(--color-primary)]" />
|
||||
<h2 className="text-2xl font-semibold text-[var(--color-heading)]">
|
||||
先连接 X 助手
|
||||
</h2>
|
||||
<p className="mt-3 max-w-xl text-base leading-7 text-[var(--color-text-subtle)]">
|
||||
右侧助手已经默认打开。完成 Gateway 接入后,这里的中间结果区会直接显示部署、资源或日志分析结果。
|
||||
</p>
|
||||
<Link
|
||||
href="/panel/api"
|
||||
className="mt-6 inline-flex items-center gap-2 rounded-full bg-[var(--color-primary)] px-5 py-3 text-sm font-semibold text-[var(--color-primary-foreground)] shadow-[var(--shadow-sm)]"
|
||||
>
|
||||
打开接口集成
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
) : assistantMessages.length === 0 && !assistantState.streamingText ? (
|
||||
<div className="flex h-full min-h-[420px] flex-col items-center justify-center rounded-[24px] border border-dashed border-[color:var(--color-surface-border)] bg-[var(--color-surface-muted)]/40 px-6 text-center">
|
||||
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-[20px] bg-[var(--color-primary-muted)] text-[var(--color-primary)]">
|
||||
<Sparkles className="h-8 w-8" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold text-[var(--color-heading)]">
|
||||
等待 X 助手输出结果
|
||||
</h2>
|
||||
<p className="mt-3 max-w-2xl text-base leading-7 text-[var(--color-text-subtle)]">
|
||||
在右侧直接提问,或者使用上面的快捷任务。助手的分析、步骤和流式结果会实时出现在这个中间工作区。
|
||||
</p>
|
||||
<div className="mt-8 flex flex-wrap justify-center gap-3">
|
||||
{config.suggestions.map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
className="rounded-full border border-[color:var(--color-surface-border)] bg-white px-4 py-2 text-sm font-semibold text-[var(--color-text-subtle)]"
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{assistantMessages.map((message) => {
|
||||
const isUser = message.role === "user";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
className={cn("flex", isUser ? "justify-end" : "justify-start")}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"max-w-[88%] rounded-[22px] px-5 py-4 shadow-[var(--shadow-sm)]",
|
||||
isUser
|
||||
? "bg-[var(--color-primary)] text-[var(--color-primary-foreground)]"
|
||||
: "border border-[color:var(--color-surface-border)] bg-white text-[var(--color-text)]",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"prose prose-sm max-w-none break-words whitespace-pre-wrap",
|
||||
isUser ? "prose-invert" : "",
|
||||
)}
|
||||
dangerouslySetInnerHTML={{ __html: message.html }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{assistantState.streamingText ? (
|
||||
<div className="flex justify-start">
|
||||
<div className="max-w-[88%] rounded-[22px] border border-[color:var(--color-surface-border)] bg-white px-5 py-4 shadow-[var(--shadow-sm)]">
|
||||
<div
|
||||
className="prose prose-sm max-w-none break-words whitespace-pre-wrap"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: assistantState.streamingHtml,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside className="min-h-0 rounded-[26px] border border-[color:var(--color-surface-border)] bg-[var(--color-surface-elevated)] shadow-[var(--shadow-md)]">
|
||||
<OpenClawAssistantPane
|
||||
defaults={defaults}
|
||||
initialQuestion={promptSeed}
|
||||
initialQuestionKey={promptKey}
|
||||
variant="sidebar"
|
||||
showConversation={false}
|
||||
emptyConversationHint="在这里发起部署、资源或日志任务,中间区域会同步展示结果。"
|
||||
onStateChange={setAssistantState}
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ const APISIX_URL_KEYS = [
|
||||
'API_GATEWAY_URL',
|
||||
] as const
|
||||
|
||||
const APISIX_TOKEN_KEYS = ['AI_GATEWAY_ACCESS_TOKEN'] as const
|
||||
const APISIX_TOKEN_KEYS = ['AI_GATEWAY_ACCESS_TOKEN', 'AI_GATEWAY_API_KEY'] as const
|
||||
|
||||
const VAULT_URL_KEYS = ['VAULT_SERVER_URL', 'VAULT_ADDR', 'vault_addr'] as const
|
||||
const VAULT_NAMESPACE_KEYS = ['VAULT_NAMESPACE'] as const
|
||||
@ -257,7 +257,7 @@ export async function resolveApisixProbeConfig(overrides?: {
|
||||
vaultToken: overrides?.vaultToken ?? readEnvValue(...VAULT_TOKEN_KEYS) ?? '',
|
||||
vaultNamespace: overrides?.vaultNamespace ?? readEnvValue(...VAULT_NAMESPACE_KEYS) ?? '',
|
||||
vaultSecretPath: overrides?.vaultSecretPath,
|
||||
fallbackKeys: ['AI_GATEWAY_ACCESS_TOKEN', 'APISIX_AI_GATEWAY_TOKEN'],
|
||||
fallbackKeys: ['AI_GATEWAY_ACCESS_TOKEN', 'AI_GATEWAY_API_KEY', 'APISIX_AI_GATEWAY_TOKEN'],
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user