feat: 重写原生AI对话页面
This commit is contained in:
@@ -42,6 +42,10 @@ import AboutPage from "./components/pages/AboutPage";
|
|||||||
import LogViewer from "./components/pages/LogViewer";
|
import LogViewer from "./components/pages/LogViewer";
|
||||||
import PermissionsPage from "./components/pages/PermissionsPage";
|
import PermissionsPage from "./components/pages/PermissionsPage";
|
||||||
import SessionsPage from "./components/pages/SessionsPage";
|
import SessionsPage from "./components/pages/SessionsPage";
|
||||||
|
import AiChatPage from "./components/pages/AiChatPage";
|
||||||
|
import SkillsMarketplacePage from "./components/pages/SkillsMarketplacePage";
|
||||||
|
import McpMarketplacePage from "./components/pages/McpMarketplacePage";
|
||||||
|
import WorkflowMarketplacePage from "./components/pages/WorkflowMarketplacePage";
|
||||||
import MCPSettings from "./components/settings/MCPSettings";
|
import MCPSettings from "./components/settings/MCPSettings";
|
||||||
import AppSidebar, {
|
import AppSidebar, {
|
||||||
NavKey,
|
NavKey,
|
||||||
@@ -94,6 +98,10 @@ export function useI18nLang(): I18nContextValue {
|
|||||||
type TabKey =
|
type TabKey =
|
||||||
| "client"
|
| "client"
|
||||||
| "sessions"
|
| "sessions"
|
||||||
|
| "legacySessions"
|
||||||
|
| "skills"
|
||||||
|
| "mcpLibrary"
|
||||||
|
| "workflow"
|
||||||
| "mcp"
|
| "mcp"
|
||||||
| "settings"
|
| "settings"
|
||||||
| "dependencies"
|
| "dependencies"
|
||||||
@@ -989,8 +997,18 @@ function App() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (key === "skills") {
|
||||||
|
setActiveTab("skills");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (key === "mcpLibrary") {
|
if (key === "mcpLibrary") {
|
||||||
setActiveTab("mcp");
|
setActiveTab("mcpLibrary");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "workflow") {
|
||||||
|
setActiveTab("workflow");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1003,6 +1021,12 @@ function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleLegacySelect = useCallback((key: LegacyTabKey) => {
|
const handleLegacySelect = useCallback((key: LegacyTabKey) => {
|
||||||
|
if (key === "sessions") {
|
||||||
|
setActiveTab("legacySessions");
|
||||||
|
setActiveNavKey("aiChat");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setActiveTab(key);
|
setActiveTab(key);
|
||||||
const navByLegacyTab: Partial<Record<LegacyTabKey, NavKey>> = {
|
const navByLegacyTab: Partial<Record<LegacyTabKey, NavKey>> = {
|
||||||
client: "overview",
|
client: "overview",
|
||||||
@@ -1176,12 +1200,20 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{activeTab === "sessions" && (
|
{activeTab === "sessions" && (
|
||||||
|
<AiChatPage
|
||||||
|
autoNew={sessionsAutoOpen}
|
||||||
|
onAutoNewConsumed={() => setSessionsAutoOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeTab === "legacySessions" && (
|
||||||
<SessionsPage
|
<SessionsPage
|
||||||
autoOpen={sessionsAutoOpen}
|
autoOpen={false}
|
||||||
onAutoOpenConsumed={() => setSessionsAutoOpen(false)}
|
|
||||||
onWebviewChange={setWebviewActions}
|
onWebviewChange={setWebviewActions}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{activeTab === "skills" && <SkillsMarketplacePage />}
|
||||||
|
{activeTab === "mcpLibrary" && <McpMarketplacePage />}
|
||||||
|
{activeTab === "workflow" && <WorkflowMarketplacePage />}
|
||||||
{activeTab === "mcp" && <MCPSettings />}
|
{activeTab === "mcp" && <MCPSettings />}
|
||||||
{activeTab === "settings" && <SettingsPage />}
|
{activeTab === "settings" && <SettingsPage />}
|
||||||
{activeTab === "dependencies" && <DependenciesPage />}
|
{activeTab === "dependencies" && <DependenciesPage />}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,647 @@
|
|||||||
|
import { message } from "antd";
|
||||||
|
import {
|
||||||
|
AUTH_KEYS,
|
||||||
|
DEFAULT_API_TIMEOUT,
|
||||||
|
DEFAULT_SERVER_HOST,
|
||||||
|
} from "@shared/constants";
|
||||||
|
import { getDomainTokenKey } from "@shared/utils/domain";
|
||||||
|
import { getCurrentAuth } from "./auth";
|
||||||
|
import { normalizeServerHost } from "./auth";
|
||||||
|
|
||||||
|
const SUCCESS_CODE = "0000";
|
||||||
|
const AI_MESSAGE_PAGE_SIZE = 20;
|
||||||
|
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
code: string;
|
||||||
|
message?: string;
|
||||||
|
data: T;
|
||||||
|
success?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AiRequestOptions {
|
||||||
|
method?: "GET" | "POST";
|
||||||
|
data?: unknown;
|
||||||
|
params?: Record<string, unknown>;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
showError?: boolean;
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AiMessageRole = "USER" | "ASSISTANT" | "SYSTEM" | "FUNCTION";
|
||||||
|
export type AiMessageMode = "CHAT" | "THINK" | "GUID" | "QUESTION" | "ANSWER";
|
||||||
|
export type AiMessageType = "USER" | "ASSISTANT" | "SYSTEM" | "TOOL";
|
||||||
|
export type AiMessageStatus =
|
||||||
|
| "loading"
|
||||||
|
| "incomplete"
|
||||||
|
| "complete"
|
||||||
|
| "error"
|
||||||
|
| "stopped"
|
||||||
|
| null;
|
||||||
|
export type AiConversationEventType =
|
||||||
|
| "PROCESSING"
|
||||||
|
| "MESSAGE"
|
||||||
|
| "FINAL_RESULT"
|
||||||
|
| "ERROR";
|
||||||
|
|
||||||
|
export interface AiPublishedUser {
|
||||||
|
id?: number;
|
||||||
|
name?: string;
|
||||||
|
username?: string;
|
||||||
|
nickName?: string;
|
||||||
|
avatar?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiAttachment {
|
||||||
|
fileKey: string;
|
||||||
|
fileUrl: string;
|
||||||
|
fileName: string;
|
||||||
|
mimeType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiUploadFileInfo {
|
||||||
|
uid: string;
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
key?: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiSelectedComponent {
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiManualComponent {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
icon?: string;
|
||||||
|
description?: string;
|
||||||
|
type: string;
|
||||||
|
defaultSelected?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiSkillMention {
|
||||||
|
id: number;
|
||||||
|
targetId: number;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
|
category?: string;
|
||||||
|
collect?: boolean;
|
||||||
|
publishUser?: AiPublishedUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiModelOption {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
model?: string;
|
||||||
|
isReasonModel?: number;
|
||||||
|
enabled?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiConversationAgent {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
|
type?: string;
|
||||||
|
category?: string;
|
||||||
|
manualComponents?: AiManualComponent[];
|
||||||
|
variables?: unknown[];
|
||||||
|
guidQuestionDtos?: Array<{
|
||||||
|
content?: string;
|
||||||
|
question?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}>;
|
||||||
|
openingChatMsg?: string;
|
||||||
|
openSuggest?: string;
|
||||||
|
hasPermission?: boolean;
|
||||||
|
isSandboxUnavailable?: boolean;
|
||||||
|
sandboxId?: string;
|
||||||
|
allowPrivateSandbox?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiConversationInfo {
|
||||||
|
id: number;
|
||||||
|
uid?: string;
|
||||||
|
agentId: number;
|
||||||
|
topic?: string;
|
||||||
|
summary?: string;
|
||||||
|
modified?: string;
|
||||||
|
created?: string;
|
||||||
|
agent?: AiConversationAgent;
|
||||||
|
messageList?: AiMessage[];
|
||||||
|
sandboxServerId?: string;
|
||||||
|
sandboxSessionId?: string;
|
||||||
|
taskStatus?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiProcessingInfo {
|
||||||
|
executeId?: string;
|
||||||
|
targetId?: number;
|
||||||
|
name?: string;
|
||||||
|
type?: string;
|
||||||
|
status?: string;
|
||||||
|
result?: {
|
||||||
|
executeId?: string;
|
||||||
|
success?: boolean;
|
||||||
|
error?: string;
|
||||||
|
input?: Record<string, unknown>;
|
||||||
|
[key: string]: unknown;
|
||||||
|
} | null;
|
||||||
|
cardBindConfig?: unknown;
|
||||||
|
cardData?: unknown;
|
||||||
|
subEventType?: string | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiFinalResult {
|
||||||
|
success?: boolean;
|
||||||
|
error?: string;
|
||||||
|
outputText?: string;
|
||||||
|
totalTokens?: number;
|
||||||
|
promptTokens?: number;
|
||||||
|
completionTokens?: number;
|
||||||
|
startTime?: number;
|
||||||
|
endTime?: number;
|
||||||
|
componentExecuteResults?: unknown[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiMessage {
|
||||||
|
id: string | number;
|
||||||
|
role: AiMessageRole;
|
||||||
|
type: AiMessageMode;
|
||||||
|
messageType: AiMessageType;
|
||||||
|
text?: string;
|
||||||
|
think?: string;
|
||||||
|
time?: string | number;
|
||||||
|
attachments?: AiAttachment[];
|
||||||
|
status?: AiMessageStatus;
|
||||||
|
processingList?: AiProcessingInfo[];
|
||||||
|
finalResult?: AiFinalResult;
|
||||||
|
requestId?: string;
|
||||||
|
metadata?: unknown;
|
||||||
|
componentExecutedList?: unknown[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiChatEvent {
|
||||||
|
completed?: boolean;
|
||||||
|
data: any;
|
||||||
|
error?: string;
|
||||||
|
eventType: AiConversationEventType;
|
||||||
|
requestId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendAiMessageParams {
|
||||||
|
conversationId: number;
|
||||||
|
variableParams?: Record<string, string | number>;
|
||||||
|
message: string;
|
||||||
|
attachments: AiAttachment[];
|
||||||
|
debug?: boolean;
|
||||||
|
selectedComponents: AiSelectedComponent[];
|
||||||
|
sandboxId?: string;
|
||||||
|
skillIds?: number[];
|
||||||
|
modelId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiAgentContext {
|
||||||
|
agentId: number;
|
||||||
|
configId?: number;
|
||||||
|
domain: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AiStreamController {
|
||||||
|
abort: () => void;
|
||||||
|
done: Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomId(prefix = "ai"): string {
|
||||||
|
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
||||||
|
return `${prefix}-${crypto.randomUUID()}`;
|
||||||
|
}
|
||||||
|
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBaseUrl(): Promise<string> {
|
||||||
|
const auth = await getCurrentAuth();
|
||||||
|
if (auth.userInfo?.currentDomain) {
|
||||||
|
return normalizeServerHost(auth.userInfo.currentDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
const step1 = (await window.electronAPI?.settings.get("step1_config")) as {
|
||||||
|
serverHost?: string;
|
||||||
|
} | null;
|
||||||
|
return normalizeServerHost(step1?.serverHost || DEFAULT_SERVER_HOST);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAuthToken(domain: string): Promise<string | null> {
|
||||||
|
const oneShotToken = (await window.electronAPI?.settings.get(
|
||||||
|
AUTH_KEYS.AUTH_TOKEN,
|
||||||
|
)) as string | null;
|
||||||
|
if (oneShotToken) return oneShotToken;
|
||||||
|
|
||||||
|
const domainTokenKey = getDomainTokenKey(domain);
|
||||||
|
return (await window.electronAPI?.settings.get(domainTokenKey)) as
|
||||||
|
| string
|
||||||
|
| null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAuthHeaders(
|
||||||
|
baseUrl: string,
|
||||||
|
): Promise<Record<string, string>> {
|
||||||
|
const token = await getAuthToken(baseUrl);
|
||||||
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendParams(url: string, params?: Record<string, unknown>): string {
|
||||||
|
if (!params) return url;
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null && value !== "") {
|
||||||
|
searchParams.append(key, String(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const query = searchParams.toString();
|
||||||
|
return query ? `${url}?${query}` : url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function aiRequest<T>(
|
||||||
|
path: string,
|
||||||
|
options: AiRequestOptions = {},
|
||||||
|
): Promise<T> {
|
||||||
|
const baseUrl = await getBaseUrl();
|
||||||
|
const authHeaders = await getAuthHeaders(baseUrl);
|
||||||
|
const finalUrl = appendParams(`${baseUrl}${path}`, options.params);
|
||||||
|
const response = await fetch(finalUrl, {
|
||||||
|
method: options.method || "POST",
|
||||||
|
credentials: "include",
|
||||||
|
cache: "no-store",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json, text/plain, */*",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...authHeaders,
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(options.timeout || DEFAULT_API_TIMEOUT),
|
||||||
|
...(options.data ? { body: JSON.stringify(options.data) } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
if (options.showError !== false) message.error(error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = (await response.json()) as ApiResponse<T>;
|
||||||
|
if (result.code !== SUCCESS_CODE) {
|
||||||
|
const errorMessage = result.message || "请求失败";
|
||||||
|
if (options.showError !== false) message.error(errorMessage);
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSandboxAgentId(configId?: number): Promise<number | null> {
|
||||||
|
if (!configId) return null;
|
||||||
|
const configs = await aiRequest<Array<{ id: number; agentId?: number }>>(
|
||||||
|
"/api/sandbox/config/list",
|
||||||
|
{ method: "GET", showError: false },
|
||||||
|
);
|
||||||
|
const current = configs.find((item) => item.id === configId);
|
||||||
|
return current?.agentId || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveAiAgentContext(): Promise<AiAgentContext> {
|
||||||
|
const auth = await getCurrentAuth();
|
||||||
|
if (!auth.isLoggedIn) {
|
||||||
|
throw new Error("请先登录");
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain = await getBaseUrl();
|
||||||
|
const configId = auth.userInfo?.id;
|
||||||
|
let agentId = auth.userInfo?.agentId || null;
|
||||||
|
|
||||||
|
if (!agentId) {
|
||||||
|
agentId = await fetchSandboxAgentId(configId);
|
||||||
|
if (agentId && auth.userInfo) {
|
||||||
|
await window.electronAPI?.settings.set(AUTH_KEYS.USER_INFO, {
|
||||||
|
...auth.userInfo,
|
||||||
|
agentId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!agentId) {
|
||||||
|
throw new Error("当前客户端未绑定智能体,请重新登录或同步配置");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { agentId, configId, domain };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAiConversations(params: {
|
||||||
|
agentId: number;
|
||||||
|
lastId?: number | null;
|
||||||
|
limit?: number;
|
||||||
|
topic?: string;
|
||||||
|
}): Promise<AiConversationInfo[]> {
|
||||||
|
return aiRequest<AiConversationInfo[]>("/api/agent/conversation/list", {
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
agentId: params.agentId,
|
||||||
|
lastId: params.lastId ?? null,
|
||||||
|
limit: params.limit || AI_MESSAGE_PAGE_SIZE,
|
||||||
|
topic: params.topic || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAiConversation(
|
||||||
|
agentId: number,
|
||||||
|
): Promise<AiConversationInfo> {
|
||||||
|
return aiRequest<AiConversationInfo>("/api/agent/conversation/create", {
|
||||||
|
method: "POST",
|
||||||
|
data: { agentId, devMode: false },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAiConversationDetail(
|
||||||
|
conversationId: number,
|
||||||
|
): Promise<AiConversationInfo> {
|
||||||
|
return aiRequest<AiConversationInfo>(
|
||||||
|
`/api/agent/conversation/${conversationId}`,
|
||||||
|
{ method: "POST" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAiMessages(params: {
|
||||||
|
conversationId: number;
|
||||||
|
index?: number;
|
||||||
|
size?: number;
|
||||||
|
}): Promise<AiMessage[]> {
|
||||||
|
return aiRequest<AiMessage[]>("/api/agent/conversation/message/list", {
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
conversationId: params.conversationId,
|
||||||
|
index: params.index || Number.MAX_SAFE_INTEGER,
|
||||||
|
size: params.size || AI_MESSAGE_PAGE_SIZE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stopAiConversation(requestId: string): Promise<void> {
|
||||||
|
await aiRequest<null>(
|
||||||
|
`/api/agent/conversation/chat/stop/${encodeURIComponent(requestId)}`,
|
||||||
|
{ method: "POST" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAiConversationTopic(params: {
|
||||||
|
id: number;
|
||||||
|
firstMessage?: string;
|
||||||
|
topic?: string;
|
||||||
|
}): Promise<AiConversationInfo> {
|
||||||
|
return aiRequest<AiConversationInfo>("/api/agent/conversation/update", {
|
||||||
|
method: "POST",
|
||||||
|
data: params,
|
||||||
|
showError: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAiModelOptions(
|
||||||
|
agentId: number,
|
||||||
|
): Promise<AiModelOption[]> {
|
||||||
|
return aiRequest<AiModelOption[]>(
|
||||||
|
`/api/agent/conversation/model/options/${agentId}`,
|
||||||
|
{ method: "GET", showError: false },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAtSkills(params: {
|
||||||
|
type: "all" | "recent" | "favorite";
|
||||||
|
kw?: string;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}): Promise<AiSkillMention[]> {
|
||||||
|
const data = {
|
||||||
|
page: params.page || 1,
|
||||||
|
pageSize: params.pageSize || 20,
|
||||||
|
kw: params.kw || undefined,
|
||||||
|
targetType: "Skill",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (params.type === "favorite") {
|
||||||
|
return aiRequest<AiSkillMention[]>("/api/published/skill/collect/list", {
|
||||||
|
method: "POST",
|
||||||
|
data,
|
||||||
|
showError: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.type === "recent") {
|
||||||
|
return aiRequest<AiSkillMention[]>(
|
||||||
|
"/api/published/skill/recentlyUsed/list",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
data,
|
||||||
|
showError: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = await aiRequest<{ records?: AiSkillMention[] }>(
|
||||||
|
"/api/published/skill/list-for-at",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
data,
|
||||||
|
showError: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return page.records || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadAiAttachment(
|
||||||
|
file: File,
|
||||||
|
): Promise<AiUploadFileInfo> {
|
||||||
|
const baseUrl = await getBaseUrl();
|
||||||
|
const authHeaders = await getAuthHeaders(baseUrl);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
formData.append("type", "tmp");
|
||||||
|
|
||||||
|
const response = await fetch(`${baseUrl}/api/file/upload`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
...authHeaders,
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`上传失败: HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = (await response.json()) as ApiResponse<{
|
||||||
|
key?: string;
|
||||||
|
url?: string;
|
||||||
|
fileName?: string;
|
||||||
|
mimeType?: string;
|
||||||
|
size?: number;
|
||||||
|
}>;
|
||||||
|
if (result.code !== SUCCESS_CODE) {
|
||||||
|
throw new Error(result.message || "上传失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
uid: randomId("file"),
|
||||||
|
key: result.data?.key || "",
|
||||||
|
url: result.data?.url || "",
|
||||||
|
name: result.data?.fileName || file.name,
|
||||||
|
type: result.data?.mimeType || file.type,
|
||||||
|
size: result.data?.size || file.size,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSseBlock(block: string): unknown | null {
|
||||||
|
const dataLines = block
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.filter((line) => line.startsWith("data:"))
|
||||||
|
.map((line) => line.replace(/^data:\s?/, ""));
|
||||||
|
if (!dataLines.length) return null;
|
||||||
|
const payload = dataLines.join("\n").trim();
|
||||||
|
if (!payload || payload === "[DONE]") return null;
|
||||||
|
return JSON.parse(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendAiMessageStream(
|
||||||
|
params: SendAiMessageParams,
|
||||||
|
handlers: {
|
||||||
|
onOpen?: () => void;
|
||||||
|
onEvent: (event: AiChatEvent) => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
onClose?: (reason: "complete" | "abort" | "error") => void;
|
||||||
|
},
|
||||||
|
): AiStreamController {
|
||||||
|
const controller = new AbortController();
|
||||||
|
let closeReason: "complete" | "abort" | "error" = "complete";
|
||||||
|
let closed = false;
|
||||||
|
|
||||||
|
const closeOnce = (reason: "complete" | "abort" | "error") => {
|
||||||
|
if (closed) return;
|
||||||
|
closed = true;
|
||||||
|
closeReason = reason;
|
||||||
|
handlers.onClose?.(reason);
|
||||||
|
};
|
||||||
|
|
||||||
|
const done = (async () => {
|
||||||
|
try {
|
||||||
|
const baseUrl = await getBaseUrl();
|
||||||
|
const authHeaders = await getAuthHeaders(baseUrl);
|
||||||
|
const response = await fetch(`${baseUrl}/api/agent/conversation/chat`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
cache: "no-store",
|
||||||
|
headers: {
|
||||||
|
Accept: "text/event-stream, application/json, text/plain, */*",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...authHeaders,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`SSE HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers.onOpen?.();
|
||||||
|
|
||||||
|
if (!response.body) {
|
||||||
|
throw new Error("当前环境不支持流式响应");
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = "";
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { value, done: readerDone } = await reader.read();
|
||||||
|
if (readerDone) break;
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
const blocks = buffer.split(/\r?\n\r?\n/);
|
||||||
|
buffer = blocks.pop() || "";
|
||||||
|
|
||||||
|
for (const block of blocks) {
|
||||||
|
const parsed = parseSseBlock(block);
|
||||||
|
if (!parsed) continue;
|
||||||
|
const event = parsed as AiChatEvent;
|
||||||
|
handlers.onEvent(event);
|
||||||
|
if (event.completed === true) {
|
||||||
|
controller.abort();
|
||||||
|
closeOnce("complete");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffer.trim()) {
|
||||||
|
const parsed = parseSseBlock(buffer);
|
||||||
|
if (parsed) handlers.onEvent(parsed as AiChatEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeOnce(closeReason);
|
||||||
|
} catch (error) {
|
||||||
|
if (controller.signal.aborted) {
|
||||||
|
closeOnce(closeReason === "complete" ? "abort" : closeReason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closeOnce("error");
|
||||||
|
handlers.onError?.(
|
||||||
|
error instanceof Error ? error : new Error(String(error)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return {
|
||||||
|
abort: () => {
|
||||||
|
closeReason = "abort";
|
||||||
|
controller.abort();
|
||||||
|
},
|
||||||
|
done,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createLocalUserMessage(
|
||||||
|
messageText: string,
|
||||||
|
attachments: AiAttachment[],
|
||||||
|
): AiMessage {
|
||||||
|
return {
|
||||||
|
id: randomId("user"),
|
||||||
|
role: "USER",
|
||||||
|
type: "CHAT",
|
||||||
|
text: messageText,
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
attachments,
|
||||||
|
messageType: "USER",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createLocalAssistantMessage(): AiMessage {
|
||||||
|
return {
|
||||||
|
id: randomId("assistant"),
|
||||||
|
role: "ASSISTANT",
|
||||||
|
type: "CHAT",
|
||||||
|
text: "",
|
||||||
|
think: "",
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
messageType: "ASSISTANT",
|
||||||
|
status: "loading",
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -217,6 +217,8 @@ export interface ClientRegisterResponse {
|
|||||||
serverPort?: number;
|
serverPort?: number;
|
||||||
/** 登录态 token,用于 webview cookie 同步 */
|
/** 登录态 token,用于 webview cookie 同步 */
|
||||||
token?: string;
|
token?: string;
|
||||||
|
/** 给当前客户端沙盒配置分配的智能体 ID,用于原生 AI 对话 */
|
||||||
|
agentId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export interface AuthUserInfo {
|
|||||||
email?: string;
|
email?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
currentDomain?: string;
|
currentDomain?: string;
|
||||||
|
agentId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 存储辅助函数 ===
|
// ========== 存储辅助函数 ===
|
||||||
@@ -335,6 +336,7 @@ export async function loginAndRegister(
|
|||||||
username,
|
username,
|
||||||
displayName: response.name,
|
displayName: response.name,
|
||||||
currentDomain: domain,
|
currentDomain: domain,
|
||||||
|
agentId: response.agentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
await setOnlineStatus(response.online);
|
await setOnlineStatus(response.online);
|
||||||
@@ -503,6 +505,15 @@ export async function reRegisterClient(): Promise<ClientRegisterResponse | null>
|
|||||||
await setConfigKey(response.configKey);
|
await setConfigKey(response.configKey);
|
||||||
await setSavedKey(response.configKey, domain, username || undefined);
|
await setSavedKey(response.configKey, domain, username || undefined);
|
||||||
await setOnlineStatus(response.online);
|
await setOnlineStatus(response.online);
|
||||||
|
const currentUserInfo = await getUserInfo();
|
||||||
|
await setUserInfo({
|
||||||
|
...currentUserInfo,
|
||||||
|
id: response.id,
|
||||||
|
username: username || "",
|
||||||
|
displayName: response.name,
|
||||||
|
currentDomain: domain || currentUserInfo?.currentDomain,
|
||||||
|
agentId: response.agentId ?? currentUserInfo?.agentId,
|
||||||
|
} as AuthUserInfo);
|
||||||
|
|
||||||
// 持久化 token(用于 webview cookie 同步)
|
// 持久化 token(用于 webview cookie 同步)
|
||||||
// 尝试立即同步,成功后清除;失败时保留给后续页面打开时重试
|
// 尝试立即同步,成功后清除;失败时保留给后续页面打开时重试
|
||||||
@@ -667,6 +678,7 @@ export async function syncConfigToServer(options?: {
|
|||||||
username: username || "",
|
username: username || "",
|
||||||
displayName: response.name,
|
displayName: response.name,
|
||||||
currentDomain: preservedCurrentDomain,
|
currentDomain: preservedCurrentDomain,
|
||||||
|
agentId: response.agentId ?? currentUserInfo?.agentId,
|
||||||
} as AuthUserInfo);
|
} as AuthUserInfo);
|
||||||
|
|
||||||
if (!suppressToast) {
|
if (!suppressToast) {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export interface AuthUserInfo {
|
|||||||
userId?: string;
|
userId?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
currentDomain?: string;
|
currentDomain?: string;
|
||||||
|
agentId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetupState {
|
export interface SetupState {
|
||||||
|
|||||||
@@ -0,0 +1,592 @@
|
|||||||
|
.page {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 280px minmax(0, 1fr);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
background: #f5f6fb;
|
||||||
|
color: #15171a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageCenter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.threadRail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 16px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-right: 1px solid #dfe3e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newButton {
|
||||||
|
width: 100%;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #00746f;
|
||||||
|
border-color: #00746f;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
margin-top: 12px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f4f7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recentLabel {
|
||||||
|
margin: 14px 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8c959f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversationList {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversationItem {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 58px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
color: #1f272e;
|
||||||
|
text-align: left;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversationItem:hover {
|
||||||
|
background: #f0f6f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversationItem.active {
|
||||||
|
color: #006f6b;
|
||||||
|
background: #e4f5f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversationTitle {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversationTime {
|
||||||
|
color: #98a1a9;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listLoading,
|
||||||
|
.listEmpty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatShell {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 56px minmax(0, 1fr) auto;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
background: #f5f6fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0 24px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-bottom: 1px solid #dfe3e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #68717b;
|
||||||
|
font-size: 13px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb span {
|
||||||
|
margin: 0 8px;
|
||||||
|
color: #a1a9b2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb strong {
|
||||||
|
color: #15171a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modelBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
max-width: 240px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #303840;
|
||||||
|
font-size: 13px;
|
||||||
|
background: #eef1f1;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modelBadge span {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background: #00c896;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatBody {
|
||||||
|
min-height: 0;
|
||||||
|
padding: 28px 32px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyChat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyAvatar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 28px;
|
||||||
|
background: #dcecef;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyChat h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #111318;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyChat p {
|
||||||
|
max-width: 520px;
|
||||||
|
margin: 12px 0 0;
|
||||||
|
color: #737c86;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestionList {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
width: min(560px, 100%);
|
||||||
|
margin-top: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestionList button {
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: #1f272e;
|
||||||
|
text-align: left;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e2e6ea;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestionList button:hover {
|
||||||
|
border-color: #00746f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageRow {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userRow {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistantRow {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistantAvatar {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #00746f;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
background: #dcecef;
|
||||||
|
border-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageBubble {
|
||||||
|
max-width: min(720px, 76%);
|
||||||
|
padding: 12px 14px;
|
||||||
|
color: #1a2026;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 0 rgb(21 23 26 / 4%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.userRow .messageBubble {
|
||||||
|
color: #ffffff;
|
||||||
|
background: #00746f;
|
||||||
|
border-radius: 12px 12px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageText {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.65;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinkBlock {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
color: #5d6873;
|
||||||
|
background: #f1f4f6;
|
||||||
|
border-left: 3px solid #b6c4c7;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinkBlock strong {
|
||||||
|
color: #38434d;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachmentList {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
max-width: 240px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
background: rgb(255 255 255 / 16%);
|
||||||
|
border: 1px solid rgb(255 255 255 / 20%);
|
||||||
|
border-radius: 7px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistantRow .attachment {
|
||||||
|
color: #38434d;
|
||||||
|
background: #f3f6f7;
|
||||||
|
border-color: #e2e7ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.processingList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.processingItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
color: #56616c;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #f4f7f8;
|
||||||
|
border-radius: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.processingName {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.processingStatus {
|
||||||
|
margin-left: auto;
|
||||||
|
color: #8a949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageStatus,
|
||||||
|
.messageError {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #76818c;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageError {
|
||||||
|
color: #c23b3b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageActions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadMoreWrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer {
|
||||||
|
padding: 14px 32px 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-top: 1px solid #dfe3e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chipRow {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
max-width: 240px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #1f5555;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #e5f4f2;
|
||||||
|
border: 1px solid #c6e5e0;
|
||||||
|
border-radius: 999px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 72px;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sendButton {
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: #00746f;
|
||||||
|
border-color: #00746f;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composerToolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 30px;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #8c959f;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbarSpacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modelSelect {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hiddenInput {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skillPanel,
|
||||||
|
.mcpPanel {
|
||||||
|
width: 320px;
|
||||||
|
max-height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skillList,
|
||||||
|
.mcpPanel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skillItem,
|
||||||
|
.mcpItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 48px;
|
||||||
|
padding: 8px;
|
||||||
|
color: #202932;
|
||||||
|
text-align: left;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e4e8eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skillItem:hover,
|
||||||
|
.mcpItem:hover,
|
||||||
|
.skillItem.selected,
|
||||||
|
.mcpItem.selected {
|
||||||
|
border-color: #00746f;
|
||||||
|
background: #edf8f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skillIcon {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #00746f;
|
||||||
|
background: #e5f4f2;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skillIcon img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skillMeta,
|
||||||
|
.mcpItem span {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skillMeta strong,
|
||||||
|
.mcpItem strong {
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.3;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skillMeta small,
|
||||||
|
.mcpItem small {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #7a858f;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.3;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.page {
|
||||||
|
grid-template-columns: 236px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.threadRail {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatHeader,
|
||||||
|
.chatBody,
|
||||||
|
.composer {
|
||||||
|
padding-left: 18px;
|
||||||
|
padding-right: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageBubble {
|
||||||
|
max-width: 86%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.page {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.threadRail {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestionList {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user