diff --git a/app/web/src/hooks/useHubCustomAgents.ts b/app/web/src/hooks/useHubCustomAgents.ts new file mode 100644 index 00000000..1660eaac --- /dev/null +++ b/app/web/src/hooks/useHubCustomAgents.ts @@ -0,0 +1,86 @@ +import { useEffect, useState } from 'react'; +import { + createHubClient, + type HubCustomAgent, +} from '@shared/index'; +import { getHubBaseUrl } from './useHubSession'; + +export type { HubCustomAgent }; + +type CustomAgentsState = { + agents: HubCustomAgent[]; + error?: string; + isLoading: boolean; + source: 'hub' | 'catalog'; +}; + +function formatHubError(error: unknown): string { + if (error instanceof Error) return error.message; + return String(error || 'Hub custom agent catalog unavailable'); +} + +export function useHubCustomAgents(token: string | null) { + const [state, setState] = useState({ + agents: [], + isLoading: false, + source: 'catalog', + }); + + useEffect(() => { + if (!token) { + setState({ agents: [], isLoading: false, source: 'catalog' }); + return undefined; + } + + const controller = new AbortController(); + let cancelled = false; + const timeoutId = window.setTimeout(() => controller.abort(), 2500); + + setState((current) => ({ ...current, isLoading: true, error: undefined })); + + const client = createHubClient({ + baseUrl: getHubBaseUrl(), + fetch: (input, init) => fetch(input, { ...init, signal: controller.signal }), + getToken: () => token, + }); + + client + .listCustomAgents() + .then((agents) => { + if (cancelled) return; + setState({ + agents, + isLoading: false, + source: 'hub', + }); + }) + .catch((error) => { + if (cancelled) return; + if (controller.signal.aborted) { + setState({ + agents: [], + error: 'Hub custom agent catalog timed out', + isLoading: false, + source: 'catalog', + }); + return; + } + + setState({ + agents: [], + error: formatHubError(error), + isLoading: false, + source: 'catalog', + }); + }) + .finally(() => window.clearTimeout(timeoutId)); + + return () => { + cancelled = true; + window.clearTimeout(timeoutId); + controller.abort(); + }; + }, [token]); + + return state; +} diff --git a/app/web/src/hooks/useHubIMSnapshot.ts b/app/web/src/hooks/useHubIMSnapshot.ts new file mode 100644 index 00000000..5217eedb --- /dev/null +++ b/app/web/src/hooks/useHubIMSnapshot.ts @@ -0,0 +1,113 @@ +import { useEffect, useState } from 'react'; +import { + createHubClient, + type HubMessage, + type HubSession, +} from '@shared/index'; +import { getHubBaseUrl } from './useHubSession'; + +type HubIMSnapshotStatus = 'locked' | 'loading' | 'ready' | 'error'; + +export type HubIMSnapshot = { + error?: string; + messagesBySessionId: Record; + sessions: HubSession[]; + status: HubIMSnapshotStatus; +}; + +const emptyMessagesBySessionId: Record = {}; + +function formatHubError(error: unknown): string { + if (error instanceof Error) return error.message; + return String(error || 'Hub IM snapshot unavailable'); +} + +function getSessionId(session: HubSession): string { + return session.session_id || session.id || ''; +} + +export function useHubIMSnapshot(token: string | null): HubIMSnapshot { + const [snapshot, setSnapshot] = useState({ + messagesBySessionId: emptyMessagesBySessionId, + sessions: [], + status: token ? 'loading' : 'locked', + }); + + useEffect(() => { + if (!token) { + setSnapshot({ + messagesBySessionId: emptyMessagesBySessionId, + sessions: [], + status: 'locked', + }); + return undefined; + } + + const controller = new AbortController(); + const timeoutId = window.setTimeout(() => controller.abort(), 3500); + let cancelled = false; + + const client = createHubClient({ + baseUrl: getHubBaseUrl(), + fetch: (input, init) => fetch(input, { ...init, signal: controller.signal }), + getToken: () => token, + }); + + setSnapshot((current) => ({ + ...current, + error: undefined, + messagesBySessionId: emptyMessagesBySessionId, + sessions: [], + status: 'loading', + })); + + async function loadSnapshot() { + try { + const sessions = (await client.listSessions()).filter( + (session) => session.type === 'private', + ); + const messageEntries = await Promise.all( + sessions.map(async (session) => { + const sessionId = getSessionId(session); + if (!sessionId) return ['', []] as const; + const messages = await client.getMessages(sessionId, { limit: 20 }); + return [sessionId, messages] as const; + }), + ); + + if (cancelled) return; + + setSnapshot({ + messagesBySessionId: Object.fromEntries( + messageEntries.filter(([sessionId]) => Boolean(sessionId)), + ), + sessions, + status: 'ready', + }); + } catch (error) { + if (cancelled) return; + + setSnapshot({ + error: controller.signal.aborted + ? 'Hub IM snapshot timed out' + : formatHubError(error), + messagesBySessionId: emptyMessagesBySessionId, + sessions: [], + status: 'error', + }); + } finally { + window.clearTimeout(timeoutId); + } + } + + void loadSnapshot(); + + return () => { + cancelled = true; + window.clearTimeout(timeoutId); + controller.abort(); + }; + }, [token]); + + return snapshot; +} diff --git a/app/web/src/hooks/useHubSession.ts b/app/web/src/hooks/useHubSession.ts new file mode 100644 index 00000000..15e71bcc --- /dev/null +++ b/app/web/src/hooks/useHubSession.ts @@ -0,0 +1,56 @@ +import { useEffect, useMemo, useState } from 'react'; + +const HUB_TOKEN_KEYS = [ + 'agenthub_hub_token', + 'agenthub:web_hub_token', + 'agenthub_web_hub_token', + 'agenthub:hub_token', +]; + +function readStorageToken(storage: Storage | undefined, key: string): string | null { + try { + return storage?.getItem(key)?.trim() || null; + } catch { + return null; + } +} + +export function getWebHubToken(): string | null { + if (typeof window === 'undefined') return null; + + for (const key of HUB_TOKEN_KEYS) { + const token = readStorageToken(window.sessionStorage, key) ?? readStorageToken(window.localStorage, key); + if (token) return token; + } + + return null; +} + +export function getHubBaseUrl(): string { + const configured = (import.meta as ImportMeta & { env?: Record }).env?.VITE_HUB_URL; + return (configured || 'http://localhost:8080').replace(/\/+$/, ''); +} + +export function useHubSession() { + const [token, setToken] = useState(() => getWebHubToken()); + + useEffect(() => { + const refresh = () => setToken(getWebHubToken()); + window.addEventListener('storage', refresh); + window.addEventListener('focus', refresh); + + return () => { + window.removeEventListener('storage', refresh); + window.removeEventListener('focus', refresh); + }; + }, []); + + return useMemo( + () => ({ + hasSession: Boolean(token), + token, + hubBaseUrl: getHubBaseUrl(), + }), + [token], + ); +} diff --git a/app/web/src/hooks/useWorkbenchProjection.ts b/app/web/src/hooks/useWorkbenchProjection.ts new file mode 100644 index 00000000..aaf59b98 --- /dev/null +++ b/app/web/src/hooks/useWorkbenchProjection.ts @@ -0,0 +1,102 @@ +import { useEffect, useReducer } from 'react'; +import { + listApprovals, + listArtifacts, + listPreviews, + listProjects, + listRunners, + listRuns, + listThreads, + workbenchReducer, + type WorkbenchState, +} from '@shared/index'; + +const initialWorkbenchProjectionState: WorkbenchState = { + projects: [], + threads: [], + runners: [], + runs: [], + threadItems: [], + approvals: [], + artifacts: [], + previews: [], + runLogs: {}, + connection: { status: 'idle' }, + lastSeq: 0, +}; + +function formatError(error: unknown) { + if (error instanceof Error) return error.message; + return String(error || 'Edge catalog unavailable'); +} + +function withTimeout(promise: Promise, timeoutMs = 2500): Promise { + return new Promise((resolve, reject) => { + const timer = window.setTimeout(() => reject(new Error('Edge catalog did not respond.')), timeoutMs); + promise.then( + (value) => { + window.clearTimeout(timer); + resolve(value); + }, + (error) => { + window.clearTimeout(timer); + reject(error); + }, + ); + }); +} + +export function useWorkbenchProjection() { + const [state, dispatch] = useReducer( + workbenchReducer, + initialWorkbenchProjectionState, + (initialState) => workbenchReducer(initialState, { type: 'connection.loading' }), + ); + + useEffect(() => { + let cancelled = false; + + async function loadSnapshot() { + dispatch({ type: 'connection.loading' }); + try { + const [projects, threads, runners, runs, approvals, artifacts, previews] = + await withTimeout(Promise.all([ + listProjects({ pageSize: 50 }), + listThreads({ pageSize: 50 }), + listRunners(), + listRuns({ pageSize: 50 }), + listApprovals(), + listArtifacts(), + listPreviews(), + ])); + + if (cancelled) return; + + dispatch({ + type: 'snapshot.loaded', + snapshot: { + projects, + threads, + runners, + runs, + approvals, + artifacts, + previews, + }, + }); + } catch (error) { + if (!cancelled) { + dispatch({ type: 'connection.error', error: formatError(error) }); + } + } + } + + loadSnapshot(); + + return () => { + cancelled = true; + }; + }, []); + + return state; +} diff --git a/app/web/src/i18n/index.ts b/app/web/src/i18n/index.ts index 5b6e722a..8fa7cdee 100644 --- a/app/web/src/i18n/index.ts +++ b/app/web/src/i18n/index.ts @@ -1,20 +1,98 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; -import zh from './locales/zh.json'; -import en from './locales/en.json'; -function detectLanguage(): string { +import zhCommon from './locales/zh/common.json'; +import zhStatus from './locales/zh/status.json'; +import zhWorkbench from './locales/zh/workbench.json'; +import zhAgentSquare from './locales/zh/agentSquare.json'; +import zhPrivateChats from './locales/zh/privateChats.json'; +import zhGroupWorkspace from './locales/zh/groupWorkspace.json'; +import zhProject from './locales/zh/project.json'; + +import enCommon from './locales/en/common.json'; +import enStatus from './locales/en/status.json'; +import enWorkbench from './locales/en/workbench.json'; +import enAgentSquare from './locales/en/agentSquare.json'; +import enPrivateChats from './locales/en/privateChats.json'; +import enGroupWorkspace from './locales/en/groupWorkspace.json'; +import enProject from './locales/en/project.json'; + +export type AppLanguage = 'en' | 'zh'; + +const LANGUAGE_STORAGE_KEY = 'agenthub-language'; + +export function normalizeLanguage(value: string | null | undefined): AppLanguage { + return value?.toLowerCase().startsWith('zh') ? 'zh' : 'en'; +} + +function isAppLanguage(value: string | null | undefined): value is AppLanguage { + return value === 'en' || value === 'zh'; +} + +function readStoredLanguage(): AppLanguage | null { + try { + const stored = localStorage.getItem(LANGUAGE_STORAGE_KEY); + return isAppLanguage(stored) ? stored : null; + } catch { + return null; + } +} + +function detectBrowserLanguage(): AppLanguage { if (typeof navigator === 'undefined') return 'en'; - const lang = navigator.language || ''; - if (lang.startsWith('zh')) return 'zh'; - return 'en'; + const language = navigator.language || navigator.languages?.[0] || ''; + return normalizeLanguage(language); +} + +export function getInitialLanguage(): AppLanguage { + return readStoredLanguage() ?? detectBrowserLanguage() ?? 'en'; +} + +export function setLanguagePreference(language: AppLanguage): void { + try { + localStorage.setItem(LANGUAGE_STORAGE_KEY, language); + } catch { + // localStorage may be unavailable in private mode or embedded contexts. + } + + void i18n.changeLanguage(language); } i18n.use(initReactI18next).init({ - resources: { zh: { translation: zh }, en: { translation: en } }, - lng: detectLanguage(), + resources: { + zh: { + common: zhCommon, + status: zhStatus, + workbench: zhWorkbench, + agentSquare: zhAgentSquare, + privateChats: zhPrivateChats, + groupWorkspace: zhGroupWorkspace, + project: zhProject, + }, + en: { + common: enCommon, + status: enStatus, + workbench: enWorkbench, + agentSquare: enAgentSquare, + privateChats: enPrivateChats, + groupWorkspace: enGroupWorkspace, + project: enProject, + }, + }, + ns: ['common', 'status', 'workbench', 'agentSquare', 'privateChats', 'groupWorkspace', 'project'], + defaultNS: 'common', + lng: getInitialLanguage(), fallbackLng: 'en', interpolation: { escapeValue: false }, }); +if (typeof document !== 'undefined') { + const syncDocumentLanguage = (language: string) => { + document.documentElement.lang = normalizeLanguage(language); + }; + + syncDocumentLanguage(i18n.resolvedLanguage || i18n.language || getInitialLanguage()); + i18n.on('languageChanged', syncDocumentLanguage); +} + export default i18n; diff --git a/app/web/src/i18n/locales/en/agentSquare.json b/app/web/src/i18n/locales/en/agentSquare.json new file mode 100644 index 00000000..1ffbe10b --- /dev/null +++ b/app/web/src/i18n/locales/en/agentSquare.json @@ -0,0 +1,100 @@ +{ + "brand.title": "AGENTHUB", + "brand.subtitle": "Agent Square", + + "sidebar.nav": "Navigation", + "sidebar.local": "Local", + "sidebar.catalog": "Catalog", + "sidebar.catalogDesc": "Browse Hub custom agents or the labeled catalog fallback", + "sidebar.workspace": "Workspace", + "sidebar.workspaceDesc": "{{count}} locally staged", + "sidebar.favorites": "Favorites", + "sidebar.favoritesDesc": "{{count}} saved agents", + "sidebar.categories": "Categories", + "sidebar.categoriesDesc": "Preview filters", + "sidebar.slots": "Workspace slots", + "sidebar.slotsDesc": "Locally staged agents stay in this preview; Hub sync is not connected.", + + "categories.all": "All agents", + + "header.title": "Catalog", + "header.subtitle": "Find the right specialist before a run starts", + "header.description": "Local catalog preview only. Hub /web/custom-agents is not connected yet.", + + "search.placeholder": "Search agents or skills", + "search.ariaLabel": "Search agents", + "search.sortLabel": "Sort agents", + + "sort.popular": "Most staged", + "sort.rating": "Highest rated", + "sort.recent": "Recently updated", + + "viewMode.all": "All agents", + "viewMode.favorites": "Only favorites", + "viewMode.installed": "Only locally staged", + + "stats.curated": "Catalog fallback agents", + "stats.hub": "Hub custom agents", + "stats.ready": "Local workspace ready", + "stats.favorites": "Favorites", + "stats.policy": "Preview checks", + + "catalog.title": "Catalog", + "catalog.subtitle": "Staged specialists", + "catalog.hubSubtitle": "Hub custom agents", + "catalog.showing": "Showing {{count}} agents", + + "filters.clear": "Clear", + "filters.clearFilters": "Clear filters", + "filters.clearedTitle": "Filters cleared", + "filters.cleared": "The catalog is back to the full local preview set.", + "source.hub": "Hub /web/custom-agents", + "source.hubDetail": "Loaded from the Hub custom-agent contract for the current Web session.", + "source.loading": "Loading Hub catalog", + "source.catalogMock": "Catalog fallback", + "source.catalogFallbackDetail": "Hub did not return custom agents, so this page is showing the labeled catalog fallback.", + "source.loginRequiredDetail": "No Web Hub session is available. Showing the labeled catalog fallback.", + "source.errorDetail": "Hub custom agents are unavailable: {{error}}. Showing the labeled catalog fallback.", + + "card.details": "Details", + "card.add": "Stage locally", + "card.added": "Locally staged", + "card.available": "Available", + "card.rating": "{{rating}} rating", + "card.installs": "{{count}} staged", + "card.saves": "{{count}} saves", + "card.favorite": "Favorite {{name}}", + "card.unfavorite": "Unfavorite {{name}}", + + "empty.noResults": "No agents match this view", + "empty.description": "Try another category, remove the search text, or clear the local filters.", + + "detail.title": "Agent detail", + "detail.close": "Close detail drawer", + "detail.rating": "Rating", + "detail.installs": "Staged count", + "detail.favorites": "Favorites", + "detail.outputs": "Expected output", + "detail.outputsPreview": "Preview", + "detail.outputsDesc": "Visible in the workspace handoff after staging.", + "detail.workspaceState": "Workspace state", + "detail.favorited": "Saved to favorites", + "detail.favoritedDesc": "Favorite state updates immediately on the card.", + "detail.notFavorited": "Not favorited", + "detail.ready": "Local workspace ready", + "detail.readyToAdd": "Ready to stage locally", + "detail.readyDesc": "Local stage state changes the card action and summary count.", + "detail.select": "Select an agent", + "detail.selectDesc": "Open details from any visible card to compare output, rating, staged count, and workspace state.", + + "confirm.added": "{{name}} saved", + "confirm.removed": "{{name}} removed from favorites", + "confirm.savesUpdated": "{{count}} local saves are now shown on the card.", + "confirm.alreadyStaged": "This agent is already staged in the current workspace.", + "confirm.alreadyStagedTitle": "{{name}} already locally staged", + "confirm.staged": "{{count}} of {{limit}} workspace slots are now staged.", + "confirm.stagedTitle": "{{name}} locally staged", + + "workspace.fullTitle": "Workspace is full", + "workspace.full": "The workspace limit is {{limit}} local agents. Remove one before adding another." +} diff --git a/app/web/src/i18n/locales/en/common.json b/app/web/src/i18n/locales/en/common.json new file mode 100644 index 00000000..69690fbf --- /dev/null +++ b/app/web/src/i18n/locales/en/common.json @@ -0,0 +1,77 @@ +{ + "brand.webPreview": "AgentHub Web Preview", + "nav.previewPages": "Preview pages", + "nav.workbench": "Workbench", + "nav.agentSquare": "Agent Square", + "nav.privateChats": "Private Chats", + "nav.groupWorkspace": "Group Workspace", + "nav.projectPreview": "Project Preview", + "shell.brand.surface": "Web shell", + "shell.toolbar.status": "Shell status", + "shell.status.edgeUnavailable": "Workbench Edge unavailable", + "shell.sidebar.label": "Workspace navigation", + "shell.sidebar.pages": "Pages", + "shell.sidebar.boundary": "Source boundary", + "shell.sidebar.boundary.detail": "This Web shell labels unavailable, locked, catalog fallback, and demo states without implying remote data is connected.", + "shell.statusPanel.label": "Status and source panel", + "shell.statusPanel.current": "Current page", + "shell.statusPanel.source": "Source state", + "shell.statusPanel.routes": "Routes", + "shell.workspace.localEdge": "Local Edge workspace", + "shell.workspace.catalog": "Agent catalog", + "shell.workspace.hubSession": "Hub session", + "shell.workspace.group": "Group workspace", + "shell.workspace.project": "Project workspace", + "shell.page.workbench.description": "Workbench Edge unavailable/error: the Web shell cannot confirm a Local Edge workbench snapshot for this session.", + "shell.page.agentSquare.description": "Agent Square catalog fallback: the page may show labeled catalog fallback content when Hub custom agents are unavailable.", + "shell.page.privateChats.description": "Private Chats Hub session required: private conversations stay locked until the Web client receives a Hub session.", + "shell.page.groupWorkspace.description": "Group demo fallback: group workspace content is clearly labeled demo data when Hub collaboration data is unavailable.", + "shell.page.projectPreview.description": "Project demo fallback: project workspace content is clearly labeled demo data when Edge or Hub project data is unavailable.", + "surface.status.realSnapshot.label": "Real snapshot", + "surface.status.realSnapshot.description": "Rendered from a verified live response or preserved service snapshot.", + "surface.status.localSource.label": "Local source", + "surface.status.localSource.description": "Backed by browser, workspace, or local configuration state.", + "surface.status.loginLocked.label": "Hub session required", + "surface.status.loginLocked.description": "Hub session required: the shell does not claim an active session or show fake private chat data.", + "surface.status.interfaceGap.label": "Interface gap", + "surface.status.interfaceGap.description": "The entry is visible, but the client/API wiring is not complete.", + "surface.status.error.label": "Edge unavailable/error", + "surface.status.error.description": "Workbench Edge unavailable/error: no verified Local Edge workbench source is connected in this Web shell.", + "surface.status.demoFallback.label": "Demo fallback", + "surface.status.demoFallback.description": "Group/Project demo fallback: the shell labels collaboration and project preview data as demo content when real sources are unavailable.", + "surface.status.catalogFallback.label": "Catalog fallback", + "surface.status.catalogFallback.description": "Agent Square catalog fallback: Hub custom-agent data is unavailable, so the catalog state is explicitly labeled as fallback.", + "surface.web.workbench.description": "Workbench Edge unavailable/error: the Web shell cannot confirm a Local Edge workbench snapshot for this session.", + "surface.web.agentSquare.description": "Agent Square catalog fallback: the page may show labeled catalog fallback content when Hub custom agents are unavailable.", + "surface.web.privateChats.description": "Private Chats Hub session required: private conversations stay locked until the Web client receives a Hub session.", + "surface.web.groupWorkspace.description": "Group demo fallback: group workspace content is clearly labeled demo data when Hub collaboration data is unavailable.", + "surface.web.projectPreview.description": "Project demo fallback: project workspace content is clearly labeled demo data when Edge or Hub project data is unavailable.", + "shell.source.error": "Edge unavailable/error", + "shell.source.error.detail": "Workbench Edge unavailable/error: no verified Local Edge workbench source is connected in this Web shell.", + "shell.source.catalog": "Catalog fallback", + "shell.source.catalog.detail": "Agent Square catalog fallback: Hub custom-agent data is unavailable, so the catalog state is explicitly labeled as fallback.", + "shell.source.locked": "Hub session required", + "shell.source.locked.detail": "Private Chats Hub session required: the shell does not claim an active session or show fake private chat data.", + "shell.source.demo": "Demo fallback", + "shell.source.demo.detail": "Group/Project demo fallback: the shell labels collaboration and project preview data as demo content when real sources are unavailable.", + "settings.open": "Settings", + "settings.title": "Settings", + "settings.language": "Language", + "settings.theme": "Theme", + "theme.light": "Light mode", + "theme.dark": "Dark mode", + "theme.switchToLight": "Switch to light theme", + "theme.switchToDark": "Switch to dark theme", + "loading": "Loading...", + "action.startRun": "Send", + "action.clearEvents": "Clear Events", + "action.cancelRun": "Cancel", + "action.create": "Create", + "action.save": "Save", + "action.close": "Close", + "action.dismiss": "Dismiss", + "action.apply": "Apply", + "action.discard": "Discard", + "action.approve": "Approve", + "action.reject": "Reject" +} diff --git a/app/web/src/i18n/locales/en/groupWorkspace.json b/app/web/src/i18n/locales/en/groupWorkspace.json new file mode 100644 index 00000000..0b744ea6 --- /dev/null +++ b/app/web/src/i18n/locales/en/groupWorkspace.json @@ -0,0 +1,136 @@ +{ + "sidebar.brandSubtitle": "Group Workspace", + "sidebar.spaces": "Spaces", + "sidebar.space.localDryRunSync": "Local dry-run sync", + "sidebar.space.sharedFiles": "Shared Files", + "sidebar.space.documents": "{{count}} documents", + "sidebar.space.demoApprovals": "Demo fallback approvals", + "sidebar.members": "Members", + "sidebar.members.onlineBusy": "{{online}} online / {{busy}} busy", + "sidebar.workspaceHealth": "Workspace Health", + "sidebar.lastLocalSync": "Last local sync: {{time}}.", + "member.filter.all": "All", + "member.filter.online": "Online", + "member.filter.busy": "Busy", + "member.filter.offline": "Offline", + "member.filter.aria": "Filter members by status", + "member.status.online": "Online", + "member.status.busy": "Busy", + "member.status.offline": "Offline", + "member.empty": "No members match this status filter. Pick another filter or cycle a member status.", + "header.title": "Shared operations cockpit", + "header.subtitle": "Members, tasks, files, approvals, and local dry-run status stay visible in one working surface.", + "header.search": "Search", + "header.searchPlaceholder": "tasks, files, members", + "header.searchAria": "Search workspace", + "header.export": "Export", + "header.assignReview": "Assign review", + "stat.membersOnline": "Online members", + "stat.demoMembersOnline": "Demo online members", + "stat.sharedTasks": "Shared tasks", + "stat.demoSharedTasks": "Demo shared tasks", + "stat.workspaceFiles": "Workspace files", + "stat.demoWorkspaceFiles": "Demo workspace files", + "stat.dryRunReadiness": "Dry-run readiness", + "stat.demoDryRunReadiness": "Demo dry-run readiness", + "task.board": "Shared Task Board", + "task.coordinationPlan": "Current coordination plan", + "task.board.backlog": "Backlog", + "task.board.inProgress": "In progress", + "task.board.review": "Review", + "task.summary.approved": "Approved for dry-run sync. Snapshot action is unlocked.", + "task.summary.changesRequested": "Reviewer requested one visible edit before approval.", + "task.summary.synced": "Dry-run snapshot synced at {{time}}.", + "task.tag.active": "Active", + "task.tag.done": "Done", + "task.tag.queue": "Queue", + "task.tag.demoActive": "Demo active", + "task.tag.demoDone": "Demo done", + "task.tag.demoQueue": "Demo queue", + "task.owner": "Owner: {{name}}", + "task.reassign": "Reassign", + "task.progressAria": "{{title}} progress", + "activity.title": "Activity Flow", + "activity.subtitle": "Workspace pulse", + "composer.mentionAria": "Mention member", + "composer.attachAria": "Attach file", + "composer.taskAria": "Create task", + "composer.messageAria": "Workspace message", + "composer.placeholder": "Send a coordination note to this workspace...", + "composer.charactersReady": "{{count}} characters ready", + "composer.draftEmpty": "Draft is empty.", + "composer.send": "Send note", + "approval.title": "Approval", + "approval.parserReady": "Parser v2 ready", + "approval.owner": "Owner: {{name}}", + "approval.detail.approved": "Parser diff is approved. Local dry-run controls are now visible and enabled.", + "approval.detail.changesRequested": "A requested-edit state is visible on the review card and board.", + "approval.detail.awaitingApproval": "Parser diff is staged, security checks passed, and local dry-run sync remains locked until approval.", + "approval.requestEdits": "Request edits", + "approval.approve": "Approve", + "approval.status.approved": "Approved", + "approval.status.changesRequested": "Changes requested", + "approval.status.awaitingApproval": "Awaiting approval", + "sync.title": "Local Sync Status", + "sync.subtitle": "Dry-run snapshot", + "sync.readiness": "Dry-run readiness", + "sync.status.synced": "Local synced", + "sync.status.unlocked": "Unlocked", + "sync.status.locked": "Locked", + "sync.readinessAria": "Dry-run readiness", + "sync.runAgain": "Run local sync again", + "sync.run": "Run local sync", + "sync.approveToRun": "Approve to run", + "sync.checklist.filesIndexed": "Files indexed", + "sync.checklist.filesDetail": "{{count}} workspace files available.", + "sync.checklist.assignments": "Assignments visible", + "sync.checklist.assignmentsDetail": "Review owner is {{owner}}.", + "sync.checklist.lastSync": "Last local sync", + "sync.checklist.lastSyncDetail": "{{time}}.", + "sync.checklist.notSynced": "Not synced", + "files.title": "Shared Files", + "files.subtitle": "Workspace documents", + "files.addAria": "Add file", + "files.empty": "No files are visible in this workspace.", + "source.edgeSnapshot": "Edge snapshot", + "source.offlineSnapshot": "Offline snapshot", + "source.loadingSnapshot": "Loading snapshot", + "source.mockFallback": "Demo / mock fallback", + "source.snapshotUnavailable": "Snapshot unavailable", + "source.localDryRun": "Local dry-run / {{source}}", + "demo.detail": "Demo/mock fallback data: {{detail}}", + "demo.summary": "Demo/mock fallback task: {{summary}}", + "demo.role": "Demo/mock fallback member: {{role}}", + "confirm.dismiss": "Dismiss confirmation", + "confirm.ready": "Local workspace ready", + "confirm.readyDetail": "Local controls are wired for review, dry-run sync, assignment, member presence, and notes.", + "confirm.statusBarCleared": "Status bar cleared", + "confirm.statusBarClearedDetail": "The next local action will appear here.", + "confirm.approvalSaved": "Approval saved", + "confirm.approvalSavedDetail": "Parser v2 is approved. The local dry-run snapshot button is now enabled.", + "confirm.changesRequested": "Changes requested", + "confirm.changesRequestedDetail": "Approval state changed and local dry-run sync is locked while the review is open.", + "confirm.reviewReassigned": "Review reassigned", + "confirm.reviewReassignedDetail": "Parser v2 is now assigned to {{owner}}.", + "confirm.syncLocked": "Local sync is locked", + "confirm.syncLockedDetail": "Approve parser v2 before running the local dry-run snapshot.", + "confirm.snapshotSynced": "Dry-run snapshot synced", + "confirm.snapshotSyncedDetail": "Local files, progress, and last dry-run time now reflect revision {{revision}}.", + "confirm.memberStatusUpdated": "Member status updated", + "confirm.memberStatusUpdatedDetail": "{{name}} switched to {{status}}.", + "confirm.memberFilterChanged": "Member filter changed", + "confirm.memberFilterAll": "Showing every workspace member.", + "confirm.memberFilterSpecific": "Showing only members marked {{filter}}.", + "confirm.noteEmpty": "Note is empty", + "confirm.noteEmptyDetail": "Write a collaboration note before sending it to the activity flow.", + "confirm.notePosted": "Note posted", + "confirm.notePostedDetail": "The note was added to the activity flow and the composer was cleared.", + "confirm.composerUpdated": "Composer content was updated locally.", + "confirm.mentionInserted": "Mention inserted", + "confirm.attachmentInserted": "Attachment marker inserted", + "confirm.taskMarkerInserted": "Task marker inserted", + "confirm.filePlaceholderAdded": "File placeholder added", + "confirm.filePlaceholderDetail": "The file counter changed locally for this interactive preview.", + "confirm.exportPrepared": "Export prepared", + "confirm.exportPreparedDetail": "No file was downloaded. The action is captured in the activity flow." +} diff --git a/app/web/src/i18n/locales/en/privateChats.json b/app/web/src/i18n/locales/en/privateChats.json new file mode 100644 index 00000000..aed35467 --- /dev/null +++ b/app/web/src/i18n/locales/en/privateChats.json @@ -0,0 +1,118 @@ +{ + "brand": { + "subtitle": "Private Chats" + }, + "search": { + "ariaLabel": "Search private chats", + "placeholder": "Search people, handoffs, snippets...", + "filtering": "{{chatCount}} chats and {{messageCount}} messages match \"{{query}}\"", + "clear": "Clear search" + }, + "sidebar": { + "title": "Private Chats", + "empty": "No Hub private chats are available.", + "emptyTitle": "No private chats" + }, + "chat": { + "localPreview": "local preview thread", + "readOnly": "Hub private sessions loaded read-only from Hub", + "error": "Hub private sessions unavailable", + "loginRequired": "Hub session required", + "messagesArea": "Message thread" + }, + "hub": { + "agent": "Agent", + "user": "User", + "recalled": "[recalled]", + "privateSession": "Hub private session", + "privateSessionFallback": "Private session", + "noRecentMessages": "No recent messages.", + "noConversationsSummary": "No conversations are available." + }, + "status": { + "localMock": "Local mock", + "readOnly": "Read-only", + "loading": "Loading", + "error": "Hub error", + "locked": "Login required" + }, + "locked": { + "title": "Hub session required", + "description": "Private Chats will not show mock conversations, play mock streams, or report a fake send success until Web has a Hub session and message contracts are wired." + }, + "loading": { + "title": "Loading Hub private chats", + "description": "Reading private sessions and recent messages from Hub." + }, + "error": { + "title": "Hub unavailable", + "description": "Private Chats could not load Hub sessions: {{error}}. Mock conversations remain hidden." + }, + "header": { + "star": "Show key messages only", + "attachments": "Open attachments", + "more": "More actions" + }, + "messages": { + "empty": { + "keyMessages": "No key messages match the current view.", + "noResults": "No messages match this search in the selected conversation.", + "threadEmpty": "This Hub private chat has no recent messages." + } + }, + "key": { + "markKey": "Mark key", + "keyed": "Keyed" + }, + "code": { + "snippet": "snippet", + "local": "local" + }, + "composer": { + "placeholder": "Write a local preview note, paste a code fragment, or attach handoff context...", + "lockedPlaceholder": "Sign in to Hub before using Private Chats", + "readOnlyPlaceholder": "Private Chats are read-only in this Web pass", + "attach": "Toggle attachment panel", + "insertCode": "Insert code", + "quote": "Quote selected message", + "messageLabel": "Message {{name}}", + "voice": "Voice note unavailable in local preview", + "send": "Append", + "lockedSend": "Locked", + "readOnlySend": "Read-only", + "attachmentPanel": "Attachment panel" + }, + "context": { + "title": "Preview Context", + "review": "{{count}} Hub messages - {{progress}}% reviewed", + "open": "Open context", + "reviewTitle": "Review Progress", + "reviewDetail": "{{keyCount}} key messages and {{unread}} unread messages from the Hub private session snapshot.", + "attachments": "Attachments", + "noAttachments": "No linked attachments for this conversation yet.", + "codeSnippets": "Code Snippets", + "noSnippets": "No code snippets are linked to this private chat.", + "tags": "Visible State", + "selectedAttachments": "Selected attachments" + }, + "attachment": { + "remove": "Remove {{name}}" + }, + "notice": { + "markedRead": "{{name}} marked as read", + "selected": "Selected {{name}}", + "removed": "Removed {{name}}", + "attachmentRemoved": "{{name}} removed", + "markKey": "Marked as key: {{author}}", + "removeKey": "Removed from key: {{author}}", + "codeInserted": "Code block inserted into the local draft", + "quoted": "Quoted {{author}}'s latest context", + "draftAdded": "Local draft appended to this preview thread", + "emptyMessage": "Write a message or select an attachment before sending", + "moreActions": "More actions are local-preview only", + "contextDetails": "Context details stay in this local preview", + "loginRequired": "Sign in to Hub before using Private Chats.", + "readOnly": "Private Chats are read-only in this Web pass.", + "dismiss": "Dismiss" + } +} diff --git a/app/web/src/i18n/locales/en/project.json b/app/web/src/i18n/locales/en/project.json new file mode 100644 index 00000000..5b3e17d4 --- /dev/null +++ b/app/web/src/i18n/locales/en/project.json @@ -0,0 +1,149 @@ +{ + "sidebar": { + "navigationAria": "Project navigation", + "brandName": "AGENTHUB", + "workspaceLabel": "Project workspace", + "navOverview": "Overview", + "navTasks": "Tasks", + "navFiles": "Files", + "navNewTask": "New task" + }, + "header": { + "searchLabel": "Search", + "searchAria": "Search projects", + "searchPlaceholder": "Projects, tasks, files...", + "clearSearch": "Clear search", + "notifications": "Notifications", + "settings": "Settings", + "currentUser": "Current user" + }, + "hero": { + "eyebrow": "Project detail", + "description": "Coordinate frontend preview pages, milestones, task readiness, design files, and dry-run records with clear live, offline snapshot, and mock fallback status.", + "simulateSync": "Simulate sync", + "syncAgain": "Sync again", + "markRisksReviewed": "Mark risks reviewed", + "reopenRisks": "Reopen risks", + "newTask": "New task", + "deliveryProgress": "Delivery progress", + "deliveryProgressAria": "Delivery progress {{percent}} percent", + "openRisks": "Open risks", + "riskReviewAria": "Risk review progress", + "catalogStatus": "Catalog status" + }, + "metrics": { + "aria": "Project metrics", + "activeTasks": "Active tasks", + "demoActiveTasks": "Demo active tasks", + "milestones": "Milestones", + "demoMilestones": "Demo milestones", + "sharedFiles": "Shared files", + "demoSharedFiles": "Demo shared files", + "dryRuns": "Dry runs", + "demoDryRuns": "Demo dry runs" + }, + "board": { + "tabsAria": "Project board sections", + "overviewTitle": "Project overview ({{count}})", + "tasksTitle": "Task status ({{count}})", + "filesTitle": "Files and run records ({{files}}/{{runs}})", + "doneTotal": "{{done}} done / {{total}} total" + }, + "overview": { + "emptyTitle": "No projects match this search.", + "emptyHint": "Clear the search box to restore the overview list." + }, + "tasks": { + "newTask": "New task", + "emptyTitle": "No tasks are visible.", + "emptyHint": "Clear search or add a local task to repopulate the board." + }, + "files": { + "label": "Files", + "filterAria": "File type filters", + "emptyTitle": "No files match this filter.", + "emptyHint": "Use All or clear search to show project files." + }, + "runs": { + "label": "Runs", + "filterAria": "Run status filters", + "recordsAria": "Run records", + "emptyTitle": "No run records match this filter.", + "emptyHint": "Run a local sync or switch the run status filter." + }, + "milestones": { + "title": "Milestones" + }, + "risks": { + "title": "Risks", + "reviewed": "Reviewed", + "needsReview": "Needs review", + "review": "Review", + "reopen": "Reopen" + }, + "taskForm": { + "drawerAria": "New task panel", + "title": "New task draft", + "description": "This panel is local UI only. It demonstrates how the project page will expose task creation without connecting a backend.", + "fieldTitle": "Title", + "fieldOwner": "Owner", + "fieldNote": "Note", + "saveLocal": "Save draft locally", + "close": "Close", + "closeAria": "Close", + "validationRequired": "Title and owner are required." + }, + "status": { + "done": "Done", + "active": "Active", + "next": "Next", + "inProgress": "In progress", + "pass": "Pass", + "ready": "Ready", + "deferred": "Deferred", + "local": "Local", + "reviewed": "Reviewed", + "open": "Open", + "tracked": "Tracked", + "edited": "Edited", + "later": "Later", + "idle": "Idle", + "localSyncComplete": "Local sync complete", + "notSyncedYet": "Not synced yet" + }, + "actions": { + "start": "Start", + "markDone": "Mark done", + "reopen": "Reopen" + }, + "filter": { + "all": "All" + }, + "source": { + "edgeSnapshot": "Edge snapshot", + "offlineSnapshot": "Offline snapshot", + "loadingSnapshot": "Loading snapshot", + "mockFallback": "Demo / mock fallback", + "snapshotUnavailable": "Snapshot unavailable", + "localDryRun": "Local dry-run / {{source}}" + }, + "demo": { + "cardDetail": "Demo/mock fallback data: {{detail}}" + }, + "confirm": { + "saveValidation": "Add a task title and owner before saving.", + "taskSaved": "Saved \"{{title}}\" as a local task.", + "taskMoved": "\"{{title}}\" moved to {{status}}.", + "riskReviewed": "\"{{title}}\" marked reviewed.", + "riskReopened": "\"{{title}}\" reopened for review.", + "allRisksReviewed": "All reviewable risks are marked reviewed.", + "allRisksReopened": "Reviewable risks were reopened.", + "syncComplete": "Sync updated local run records at {{time}}.", + "fallbackNote": "No additional note was added.", + "openRisksRemaining_one": "{{count}} open risk still needs review.", + "openRisksRemaining_other": "{{count}} open risks still need review.", + "activeTasksRemaining_one": "{{count}} active task remains after local review.", + "activeTasksRemaining_other": "{{count}} active tasks remain after local review.", + "localSyncDetail": "Local sync captured {{tasks}} active tasks and {{risks}} open risks." + } +} diff --git a/app/web/src/i18n/locales/en/status.json b/app/web/src/i18n/locales/en/status.json new file mode 100644 index 00000000..05d02a19 --- /dev/null +++ b/app/web/src/i18n/locales/en/status.json @@ -0,0 +1,40 @@ +{ + "status.online": "Local Edge: Online - {{version}} ({{edgeId}})", + "status.offline": "Local Edge: Offline", + "status.wsConnected": "WS: Connected", + "status.wsDisconnected": "WS: Disconnected", + "agent.title": "Agents", + "agent.emptyOnline": "No agents available", + "agent.emptyOffline": "Waiting for Edge...", + "agent.capability.streaming": "Streaming", + "agent.capability.toolCalls": "Tool Calls", + "agent.capability.fileChanges": "File Changes", + "agent.capability.thinking": "Thinking", + "agent.capability.multiTurn": "Multi-Turn", + "thread.title": "Threads", + "thread.empty": "No threads yet", + "thread.create": "New Thread", + "thread.search": "Search threads...", + "chat.empty": "Select a thread and send a message to start", + "chat.thinking": "Thinking...", + "chat.toolCall": "Tool: {{name}}", + "chat.fileChange": "Changed: {{path}}", + "chat.sessionInit": "Session initialized - {{model}}", + "chat.result.success": "Completed - {{input}} in / {{output}} out tokens", + "chat.result.failed": "Failed: {{error}}", + "event.title": "Events", + "event.emptyOnline": "Waiting for events...", + "event.emptyOffline": "Start Edge Server to receive events", + "prompt.placeholder": "Type a message, @Agent to mention...", + "prompt.agentSelector": "Select Agent", + "run.status.queued": "Queued", + "run.status.running": "Running", + "run.status.finished": "Finished", + "run.status.failed": "Failed", + "run.status.idle": "Idle", + "run.title": "Run Detail", + "run.output": "Output", + "run.toolCalls": "Tool Calls", + "run.fileChanges": "Changed Files", + "error.streamError": "Event stream error: {{message}}" +} diff --git a/app/web/src/i18n/locales/en/workbench.json b/app/web/src/i18n/locales/en/workbench.json new file mode 100644 index 00000000..ebb7ab0c --- /dev/null +++ b/app/web/src/i18n/locales/en/workbench.json @@ -0,0 +1,162 @@ +{ + "status": { + "connected": "Connected", + "disconnected": "Disconnected", + "idle": "Idle", + "error": "Error", + "online": "Online", + "offline": "Offline", + "active": "Active", + "queued": "Queued", + "running": "Running", + "finished": "Finished", + "failed": "Failed", + "pending": "Pending", + "approved": "Approved", + "rejected": "Rejected", + "loading": "Loading", + "ready": "Ready", + "waiting_approval": "Waiting Approval" + }, + "leftRail": { + "ariaLabel": "Project and thread navigation", + "workbench": "Workbench", + "project": "Project", + "noProject": "No project loaded", + "connectEdge": "Connect Edge to load projects", + "threads": "Threads", + "noThreads": "No threads returned by Edge.", + "runners": "Runner Status", + "noRunners": "No runners are registered." + }, + "runner": { + "adapterReady": "adapter ready" + }, + "header": { + "thread": "Thread", + "noThread": "No thread selected", + "edgeUnavailable": "Edge is unavailable. Snapshot data is preserved if it was loaded.", + "subtitle": "Project, messages, run progress, approval, and artifacts stay in one review path.", + "runStatus": "Run {{status}}", + "startRun": "Start Run", + "starting": "Starting" + }, + "state": { + "loading": "Loading the Edge snapshot. Actions stay locked until the reducer has real data.", + "offlineSnapshot": "Edge is offline. Showing the last reducer snapshot; write actions are locked.", + "offlineEmpty": "Edge is unavailable and no snapshot has been loaded. No mock success state is shown.", + "empty": "No Edge snapshot data is available yet. Start Edge to load projects, threads, runners, and runs." + }, + "runSummary": { + "ariaLabel": "Run summary", + "noRun": "No run", + "artifacts": "Artifacts", + "approvalGate": "Approval gate", + "noApproval": "No approval", + "previewTarget": "Preview target", + "noPreview": "No preview" + }, + "messages": { + "ariaLabel": "Thread messages and run timeline", + "title": "IM Message Flow", + "subtitle": "@Agent collaboration", + "empty": "No messages loaded for this thread." + }, + "message": { + "author": { + "owner": "Owner", + "agent": "Agent" + }, + "empty": "(empty message)" + }, + "timeline": { + "ariaLabel": "AgentRun timeline", + "title": "AgentRun Timeline", + "queuedDone": "Thread accepted the owner request.", + "queuedWaiting": "Waiting for a run.", + "startedWaiting": "Runner has not started yet.", + "logsAvailable": "Logs are available in the workspace panel.", + "noLogs": "No logs yet.", + "artifactsAvailable": "Artifacts are reviewable.", + "noArtifacts": "No artifacts yet." + }, + "approval": { + "ariaLabel": "Approval request", + "title": "Approval request", + "empty": "No approval request", + "description": "Approval decisions are sent to Edge. The UI updates only after the API call succeeds.", + "noPending": "Edge has not returned a pending approval for this run.", + "reject": "Reject", + "approve": "Approve", + "status": { + "approved": "Approved", + "rejected": "Rejected", + "pending": "Pending approval" + } + }, + "composer": { + "attach": "Attach context", + "threadAriaLabel": "Message thread", + "placeholder": "Message this Thread with @ClaudeCode / @Codex / @OpenCode...", + "lockedPlaceholder": "Connect Edge and load a snapshot before sending", + "sending": "Sending", + "send": "Send" + }, + "workspace": { + "ariaLabel": "Workbench workspace", + "title": "Workspace", + "subtitle": "Files, Diff, Preview, Logs, Approval", + "tabs": { + "ariaLabel": "Workspace panels", + "files": "Changed Files", + "diff": "Diff", + "preview": "Preview", + "logs": "Logs", + "approval": "Approval" + }, + "summary": { + "ariaLabel": "Workspace summary", + "artifacts": "Artifacts", + "artifactCount": "{{count}} files", + "risk": "Risk", + "approvalNeeded": "Approval needed", + "noPendingGate": "No pending gate" + }, + "files": { + "fileInfo": "{{size}} KB - created {{date}}" + }, + "diff": { + "ariaLabel": "Artifact diff summary" + }, + "preview": { + "unavailable": "preview unavailable", + "ready": "Preview ready", + "notReady": "No preview ready", + "hint": "Create a preview after a run produces reviewable output.", + "request": "Request Preview", + "open": "Open Preview" + }, + "logs": { + "ariaLabel": "Run logs" + }, + "approval": { + "artifacts": "Artifacts", + "artifactsAvailable": "Artifacts are available for review.", + "noArtifacts": "No artifacts are loaded.", + "applyGate": "Apply gate", + "ownerApprovalRequired": "Owner approval is required before apply/discard.", + "noPendingRequest": "There is no pending approval request.", + "decisionSource": "Decision state comes from Edge API or WebSocket events." + }, + "empty": { + "artifacts": "No artifacts returned for this run.", + "diffArtifacts": "No artifacts have been created for this run yet.", + "logs": "No run logs returned by Edge." + } + }, + "error": { + "edgeUnavailable": "Edge is unavailable", + "edgeApiTimeout": "Edge API did not respond. Check that Edge is running on 127.0.0.1:3210.", + "edgeEventStream": "Edge event stream error" + } +} diff --git a/app/web/src/i18n/locales/zh/agentSquare.json b/app/web/src/i18n/locales/zh/agentSquare.json new file mode 100644 index 00000000..4afbeff1 --- /dev/null +++ b/app/web/src/i18n/locales/zh/agentSquare.json @@ -0,0 +1,86 @@ +{ + "brand.title": "AGENTHUB", + "brand.subtitle": "智能体广场", + "sidebar.nav": "导航", + "sidebar.local": "本地", + "sidebar.catalog": "目录", + "sidebar.catalogDesc": "浏览 Hub CustomAgent,或带明确标签的 catalog fallback", + "sidebar.workspace": "工作区", + "sidebar.workspaceDesc": "{{count}} 个已本地暂存", + "sidebar.favorites": "收藏", + "sidebar.favoritesDesc": "{{count}} 个已保存智能体", + "sidebar.categories": "分类", + "sidebar.categoriesDesc": "预览筛选器", + "sidebar.slots": "工作区槽位", + "sidebar.slotsDesc": "本地暂存的智能体保留在此预览中;Hub 同步尚未连接。", + "categories.all": "全部智能体", + "header.title": "目录", + "header.subtitle": "在运行开始前找到合适的专家", + "header.description": "仅限本地目录预览。Hub /web/custom-agents 尚未连接。", + "search.placeholder": "搜索智能体或技能", + "search.ariaLabel": "搜索智能体", + "search.sortLabel": "排序智能体", + "sort.popular": "最多暂存", + "sort.rating": "最高评分", + "sort.recent": "最近更新", + "viewMode.all": "全部智能体", + "viewMode.favorites": "仅收藏", + "viewMode.installed": "仅本地暂存", + "stats.curated": "Catalog fallback 智能体", + "stats.ready": "本地工作区就绪", + "stats.favorites": "收藏", + "stats.policy": "预览检查", + "catalog.title": "目录", + "catalog.subtitle": "已暂存的专家", + "catalog.showing": "显示 {{count}} 个智能体", + "filters.clear": "清除", + "filters.clearFilters": "清除筛选", + "filters.clearedTitle": "筛选已清除", + "filters.cleared": "目录已恢复为完整本地预览集。", + "card.details": "详情", + "card.add": "本地暂存", + "card.added": "已本地暂存", + "card.available": "可用", + "card.rating": "{{rating}} 评分", + "card.installs": "{{count}} 暂存", + "card.saves": "{{count}} 保存", + "card.favorite": "收藏 {{name}}", + "card.unfavorite": "取消收藏 {{name}}", + "empty.noResults": "没有匹配此视图的智能体", + "empty.description": "尝试其他分类,移除搜索文本,或清除本地筛选。", + "detail.title": "智能体详情", + "detail.close": "关闭详情面板", + "detail.rating": "评分", + "detail.installs": "暂存数", + "detail.favorites": "收藏", + "detail.outputs": "预期输出", + "detail.outputsPreview": "预览", + "detail.outputsDesc": "暂存后在工作区交接中可见。", + "detail.workspaceState": "工作区状态", + "detail.favorited": "已加入收藏", + "detail.favoritedDesc": "收藏状态会立即在卡片上更新。", + "detail.notFavorited": "未收藏", + "detail.ready": "本地工作区就绪", + "detail.readyToAdd": "可以本地暂存", + "detail.readyDesc": "本地暂存状态会更改卡片操作和摘要计数。", + "detail.select": "选择一个智能体", + "detail.selectDesc": "从任意可见卡片打开详情,以比较输出、评分、暂存数和工作区状态。", + "confirm.added": "{{name}} 已保存", + "confirm.removed": "{{name}} 已从收藏移除", + "confirm.savesUpdated": "卡片上现在显示 {{count}} 个本地保存。", + "confirm.alreadyStaged": "此智能体已在当前工作区中暂存。", + "confirm.alreadyStagedTitle": "{{name}} 已本地暂存", + "confirm.staged": "已暂存 {{count}}/{{limit}} 个工作区槽位。", + "confirm.stagedTitle": "{{name}} 已本地暂存", + "workspace.fullTitle": "工作区已满", + "workspace.full": "工作区限制为 {{limit}} 个本地智能体。请先移除一个再添加。", + "stats.hub": "Hub CustomAgent", + "catalog.hubSubtitle": "Hub CustomAgent", + "source.hub": "Hub /web/custom-agents", + "source.hubDetail": "已从当前 Web 会话的 Hub CustomAgent 契约加载。", + "source.loading": "正在加载 Hub 目录", + "source.catalogMock": "Catalog fallback", + "source.catalogFallbackDetail": "Hub 未返回 CustomAgent,因此显示带明确标签的 catalog fallback。", + "source.loginRequiredDetail": "当前没有 Web Hub 会话。显示带明确标签的 catalog fallback。", + "source.errorDetail": "Hub CustomAgent 不可用:{{error}}。显示带明确标签的 catalog fallback。" +} diff --git a/app/web/src/i18n/locales/zh/common.json b/app/web/src/i18n/locales/zh/common.json new file mode 100644 index 00000000..a88dffee --- /dev/null +++ b/app/web/src/i18n/locales/zh/common.json @@ -0,0 +1,77 @@ +{ + "brand.webPreview": "AgentHub Web 预览", + "nav.previewPages": "预览页面", + "nav.workbench": "工作台", + "nav.agentSquare": "智能体广场", + "nav.privateChats": "私聊", + "nav.groupWorkspace": "群组工作区", + "nav.projectPreview": "项目预览", + "shell.brand.surface": "Web 外壳", + "shell.toolbar.status": "外壳状态", + "shell.status.edgeUnavailable": "工作台 Edge 不可用", + "shell.sidebar.label": "工作区导航", + "shell.sidebar.pages": "页面", + "shell.sidebar.boundary": "数据来源边界", + "shell.sidebar.boundary.detail": "这个 Web 外壳会明确标注不可用、锁定、目录兜底和演示状态,不假装数据已同步。", + "shell.statusPanel.label": "状态和来源面板", + "shell.statusPanel.current": "当前页面", + "shell.statusPanel.source": "来源状态", + "shell.statusPanel.routes": "路由", + "shell.workspace.localEdge": "本地 Edge 工作区", + "shell.workspace.catalog": "智能体目录", + "shell.workspace.hubSession": "Hub 会话", + "shell.workspace.group": "群组工作区", + "shell.workspace.project": "项目工作区", + "shell.page.workbench.description": "工作台 Edge 不可用/错误:当前 Web 外壳无法确认本地 Edge 工作台快照。", + "shell.page.agentSquare.description": "智能体广场目录兜底:当 Hub 自定义智能体不可用时,页面只展示带明确标注的目录兜底内容。", + "shell.page.privateChats.description": "私聊需要 Hub 会话:Web 客户端拿到 Hub 会话前,私聊保持锁定。", + "shell.page.groupWorkspace.description": "群组演示兜底:Hub 协作数据不可用时,群组工作区内容会明确标注为演示数据。", + "shell.page.projectPreview.description": "项目演示兜底:Edge 或 Hub 项目数据不可用时,项目工作区内容会明确标注为演示数据。", + "surface.status.realSnapshot.label": "真实快照", + "surface.status.realSnapshot.description": "来自已验证的实时响应或保留的服务快照。", + "surface.status.localSource.label": "本地来源", + "surface.status.localSource.description": "来自浏览器、工作区或本地配置状态。", + "surface.status.loginLocked.label": "需要 Hub 会话", + "surface.status.loginLocked.description": "私聊需要 Hub 会话:外壳不会声称会话已激活,也不会显示假的私聊数据。", + "surface.status.interfaceGap.label": "接口缺口", + "surface.status.interfaceGap.description": "入口可见,但客户端或接口接线尚未完成。", + "surface.status.error.label": "Edge 不可用/错误", + "surface.status.error.description": "工作台 Edge 不可用/错误:这个 Web 外壳没有连接到已验证的本地 Edge 工作台来源。", + "surface.status.demoFallback.label": "演示兜底", + "surface.status.demoFallback.description": "群组/项目演示兜底:真实来源不可用时,协作和项目预览数据会标注为演示内容。", + "surface.status.catalogFallback.label": "目录兜底", + "surface.status.catalogFallback.description": "智能体广场目录兜底:Hub 自定义智能体数据不可用,因此目录状态会明确标注为兜底。", + "surface.web.workbench.description": "工作台 Edge 不可用/错误:当前 Web 外壳无法确认本地 Edge 工作台快照。", + "surface.web.agentSquare.description": "智能体广场目录兜底:当 Hub 自定义智能体不可用时,页面只展示带明确标注的目录兜底内容。", + "surface.web.privateChats.description": "私聊需要 Hub 会话:Web 客户端拿到 Hub 会话前,私聊保持锁定。", + "surface.web.groupWorkspace.description": "群组演示兜底:Hub 协作数据不可用时,群组工作区内容会明确标注为演示数据。", + "surface.web.projectPreview.description": "项目演示兜底:Edge 或 Hub 项目数据不可用时,项目工作区内容会明确标注为演示数据。", + "shell.source.error": "Edge 不可用/错误", + "shell.source.error.detail": "工作台 Edge 不可用/错误:这个 Web 外壳没有连接到已验证的本地 Edge 工作台来源。", + "shell.source.catalog": "目录兜底", + "shell.source.catalog.detail": "智能体广场目录兜底:Hub 自定义智能体数据不可用,因此目录状态会明确标注为兜底。", + "shell.source.locked": "需要 Hub 会话", + "shell.source.locked.detail": "私聊需要 Hub 会话:外壳不会声称会话已激活,也不会显示假的私聊数据。", + "shell.source.demo": "演示兜底", + "shell.source.demo.detail": "群组/项目演示兜底:真实来源不可用时,协作和项目预览数据会标注为演示内容。", + "settings.open": "设置", + "settings.title": "设置", + "settings.language": "语言", + "settings.theme": "主题", + "theme.light": "浅色模式", + "theme.dark": "深色模式", + "theme.switchToLight": "切换到浅色主题", + "theme.switchToDark": "切换到深色主题", + "loading": "加载中...", + "action.startRun": "发送", + "action.clearEvents": "清空事件", + "action.cancelRun": "取消", + "action.create": "新建", + "action.save": "保存", + "action.close": "关闭", + "action.dismiss": "忽略", + "action.apply": "应用", + "action.discard": "丢弃", + "action.approve": "批准", + "action.reject": "拒绝" +} diff --git a/app/web/src/i18n/locales/zh/groupWorkspace.json b/app/web/src/i18n/locales/zh/groupWorkspace.json new file mode 100644 index 00000000..75818645 --- /dev/null +++ b/app/web/src/i18n/locales/zh/groupWorkspace.json @@ -0,0 +1,138 @@ +{ + "sidebar.brandSubtitle": "群组工作区", + "sidebar.spaces": "空间", + "sidebar.space.localDryRunSync": "本地试运行同步", + "sidebar.space.sharedFiles": "共享文件", + "sidebar.space.documents": "{{count}} 份文档", + "sidebar.space.demoApprovals": "演示 fallback 审批", + "sidebar.members": "成员", + "sidebar.members.onlineBusy": "{{online}} 在线 / {{busy}} 忙碌", + "sidebar.workspaceHealth": "工作区健康度", + "sidebar.lastLocalSync": "上次本地同步:{{time}}。", + "member.filter.all": "全部", + "member.filter.online": "在线", + "member.filter.busy": "忙碌", + "member.filter.offline": "离线", + "member.filter.aria": "按状态筛选成员", + "member.status.online": "在线", + "member.status.busy": "忙碌", + "member.status.offline": "离线", + "member.empty": "没有成员匹配此状态筛选。请选择其他筛选条件或切换成员状态。", + "header.title": "共享操作驾驶舱", + "header.subtitle": "成员、任务、文件、审批和本地试运行状态在同一工作界面中一览无余。", + "header.search": "搜索", + "header.searchPlaceholder": "任务、文件、成员", + "header.searchAria": "搜索工作区", + "header.export": "导出", + "header.assignReview": "分配审查", + "stat.membersOnline": "在线成员", + "stat.demoMembersOnline": "演示在线成员", + "stat.sharedTasks": "共享任务", + "stat.demoSharedTasks": "演示共享任务", + "stat.workspaceFiles": "工作区文件", + "stat.demoWorkspaceFiles": "演示工作区文件", + "stat.dryRunReadiness": "试运行就绪度", + "stat.demoDryRunReadiness": "演示试运行就绪度", + "task.board": "共享任务看板", + "task.coordinationPlan": "当前协调计划", + "task.board.backlog": "待办", + "task.board.inProgress": "进行中", + "task.board.review": "审查", + "task.summary.approved": "已批准试运行同步。快照操作已解锁。", + "task.summary.changesRequested": "审查者请求在批准前修改一处。", + "task.summary.synced": "试运行快照已于 {{time}} 同步。", + "task.tag.active": "进行中", + "task.tag.done": "已完成", + "task.tag.queue": "排队中", + "task.tag.demoActive": "演示进行中", + "task.tag.demoDone": "演示已完成", + "task.tag.demoQueue": "演示排队中", + "task.owner": "负责人:{{name}}", + "task.reassign": "重新分配", + "task.progressAria": "{{title}} 进度", + "activity.title": "活动流", + "activity.subtitle": "工作区动态", + "composer.mentionAria": "提及成员", + "composer.attachAria": "附加文件", + "composer.taskAria": "创建任务", + "composer.messageAria": "工作区消息", + "composer.placeholder": "向此工作区发送协作消息...", + "composer.charactersReady": "{{count}} 个字符已就绪", + "composer.draftEmpty": "草稿为空。", + "composer.send": "发送消息", + "approval.title": "审批", + "approval.parserReady": "Parser v2 就绪", + "approval.owner": "负责人:{{name}}", + "approval.detail.approved": "Parser diff 已批准。本地试运行控制现已可见并启用。", + "approval.detail.changesRequested": "审查卡片和看板上显示请求修改状态。", + "approval.detail.awaitingApproval": "Parser diff 已暂存,安全检查已通过,本地试运行同步在批准前保持锁定。", + "approval.requestEdits": "请求修改", + "approval.approve": "批准", + "approval.status.approved": "已批准", + "approval.status.changesRequested": "请求修改", + "approval.status.awaitingApproval": "等待批准", + "sync.title": "本地同步状态", + "sync.subtitle": "试运行快照", + "sync.readiness": "试运行就绪度", + "sync.status.synced": "本地已同步", + "sync.status.unlocked": "已解锁", + "sync.status.locked": "已锁定", + "sync.readinessAria": "试运行就绪度", + "sync.runAgain": "再次运行本地同步", + "sync.run": "运行本地同步", + "sync.approveToRun": "批准后运行", + "sync.checklist.filesIndexed": "文件已索引", + "sync.checklist.filesDetail": "{{count}} 个工作区文件可用。", + "sync.checklist.assignments": "分配可见", + "sync.checklist.assignmentsDetail": "审查负责人为 {{owner}}。", + "sync.checklist.lastSync": "上次本地同步", + "sync.checklist.lastSyncDetail": "{{time}}。", + "sync.checklist.notSynced": "未同步", + "files.title": "共享文件", + "files.subtitle": "工作区文档", + "files.addAria": "添加文件", + "files.empty": "此工作区中没有可见文件。", + "confirm.dismiss": "关闭确认", + "confirm.ready": "本地工作区就绪", + "confirm.readyDetail": "本地控件已连接,可进行审查、试运行同步、分配、成员状态和协作消息。", + "confirm.statusBarCleared": "状态栏已清除", + "confirm.statusBarClearedDetail": "下一个本地操作将显示在此处。", + "confirm.approvalSaved": "审批已保存", + "confirm.approvalSavedDetail": "Parser v2 已批准。本地试运行快照按钮现已启用。", + "confirm.changesRequested": "已请求修改", + "confirm.changesRequestedDetail": "审批状态已更改,审查期间本地试运行同步已锁定。", + "confirm.reviewReassigned": "审查已重新分配", + "confirm.reviewReassignedDetail": "Parser v2 现已分配给 {{owner}}。", + "confirm.syncLocked": "本地同步已锁定", + "confirm.syncLockedDetail": "运行本地试运行快照前请先批准 Parser v2。", + "confirm.snapshotSynced": "试运行快照已同步", + "confirm.snapshotSyncedDetail": "本地文件、进度和上次试运行时间现已反映版本 {{revision}}。", + "confirm.memberStatusUpdated": "成员状态已更新", + "confirm.memberStatusUpdatedDetail": "{{name}} 已切换为 {{status}}。", + "confirm.memberFilterChanged": "成员筛选已更改", + "confirm.memberFilterAll": "显示所有工作区成员。", + "confirm.memberFilterSpecific": "仅显示标记为 {{filter}} 的成员。", + "confirm.noteEmpty": "消息为空", + "confirm.noteEmptyDetail": "先写一条协作消息再发送到活动流。", + "confirm.notePosted": "消息已发送", + "confirm.notePostedDetail": "消息已添加到活动流,编辑器已清空。", + "confirm.composerUpdated": "编辑器内容已本地更新。", + "confirm.mentionInserted": "提及已插入", + "confirm.attachmentInserted": "附件标记已插入", + "confirm.taskMarkerInserted": "任务标记已插入", + "confirm.filePlaceholderAdded": "文件占位符已添加", + "confirm.filePlaceholderDetail": "文件计数器已针对此交互式预览进行本地更改。", + "confirm.exportPrepared": "导出已准备", + "confirm.exportPreparedDetail": "未下载任何文件。该操作已记录在活动流中。", + "source": { + "edgeSnapshot": "Edge 快照", + "offlineSnapshot": "离线快照", + "loadingSnapshot": "正在加载快照", + "mockFallback": "演示 / mock fallback", + "snapshotUnavailable": "快照不可用", + "localDryRun": "本地 dry-run / {{source}}" + }, + "demo.detail": "演示/mock fallback 数据:{{detail}}", + "demo.summary": "演示/mock fallback 任务:{{summary}}", + "demo.role": "演示/mock fallback 成员:{{role}}" +} diff --git a/app/web/src/i18n/locales/zh/privateChats.json b/app/web/src/i18n/locales/zh/privateChats.json new file mode 100644 index 00000000..094b7a67 --- /dev/null +++ b/app/web/src/i18n/locales/zh/privateChats.json @@ -0,0 +1,118 @@ +{ + "brand": { + "subtitle": "私聊" + }, + "search": { + "ariaLabel": "搜索私聊", + "placeholder": "搜索人员、交接、代码片段...", + "filtering": "{{chatCount}} 个聊天和 {{messageCount}} 条消息匹配 \"{{query}}\"", + "clear": "清除搜索" + }, + "sidebar": { + "title": "私聊", + "empty": "没有可用的 Hub 私聊。", + "emptyTitle": "暂无私聊" + }, + "chat": { + "localPreview": "本地预览线程", + "messagesArea": "消息线程", + "readOnly": "已从 Hub 只读加载私聊会话", + "error": "Hub 私聊会话不可用", + "loginRequired": "需要 Hub 会话" + }, + "hub": { + "agent": "智能体", + "user": "用户", + "recalled": "[已撤回]", + "privateSession": "Hub 私聊会话", + "privateSessionFallback": "私聊会话", + "noRecentMessages": "暂无最近消息。", + "noConversationsSummary": "暂无可用会话。" + }, + "status": { + "localMock": "本地模拟", + "readOnly": "只读", + "loading": "加载中", + "error": "Hub 错误", + "locked": "需要登录" + }, + "locked": { + "title": "需要 Hub 会话", + "description": "在 Web 接入 Hub 会话和消息契约前,私聊不会显示 mock 会话、播放 mock 流或伪造发送成功。" + }, + "loading": { + "title": "正在加载 Hub 私聊", + "description": "正在从 Hub 读取私聊会话和最近消息。" + }, + "error": { + "title": "Hub 不可用", + "description": "Private Chats 无法加载 Hub 会话:{{error}}。mock 会话会继续隐藏。" + }, + "header": { + "star": "仅显示关键消息", + "attachments": "打开附件", + "more": "更多操作" + }, + "messages": { + "empty": { + "keyMessages": "当前视图没有匹配的关键消息。", + "noResults": "所选会话中没有匹配此搜索的消息。", + "threadEmpty": "此 Hub 私聊暂无最近消息。" + } + }, + "key": { + "markKey": "标记为关键", + "keyed": "已标记" + }, + "code": { + "snippet": "代码片段", + "local": "本地" + }, + "composer": { + "placeholder": "编写本地预览笔记、粘贴代码片段或附加交接上下文...", + "attach": "切换附件面板", + "insertCode": "插入代码", + "quote": "引用所选消息", + "messageLabel": "发送消息给 {{name}}", + "voice": "本地预览中语音备注不可用", + "send": "追加", + "attachmentPanel": "附件面板", + "lockedPlaceholder": "登录 Hub 后才能使用私聊", + "readOnlyPlaceholder": "本轮 Web 私聊保持只读", + "lockedSend": "已锁定", + "readOnlySend": "只读" + }, + "context": { + "title": "预览上下文", + "review": "{{count}} 条 Hub 消息 - 已检查 {{progress}}%", + "open": "打开上下文", + "reviewTitle": "检查进度", + "reviewDetail": "当前 Hub 私聊快照中有 {{keyCount}} 条关键消息和 {{unread}} 条未读消息。", + "attachments": "附件", + "noAttachments": "此会话暂无关联附件。", + "codeSnippets": "代码片段", + "noSnippets": "此私聊没有关联的代码片段。", + "tags": "可见状态", + "selectedAttachments": "已选附件" + }, + "attachment": { + "remove": "移除 {{name}}" + }, + "notice": { + "markedRead": "{{name}} 已标记为已读", + "selected": "已选择 {{name}}", + "removed": "已移除 {{name}}", + "attachmentRemoved": "{{name}} 已移除", + "markKey": "已标记为关键:{{author}}", + "removeKey": "已取消关键标记:{{author}}", + "codeInserted": "代码块已插入到本地草稿", + "quoted": "已引用 {{author}} 的最新上下文", + "draftAdded": "本地草稿已追加到此预览线程", + "emptyMessage": "请先编写消息或选择附件再发送", + "moreActions": "更多操作仅限本地预览", + "contextDetails": "上下文详情仅保留在本地预览中", + "dismiss": "忽略", + "loginRequired": "请先登录 Hub 再使用私聊。", + "readOnly": "本轮 Web 私聊保持只读。" + } +} diff --git a/app/web/src/i18n/locales/zh/project.json b/app/web/src/i18n/locales/zh/project.json new file mode 100644 index 00000000..7cfb8552 --- /dev/null +++ b/app/web/src/i18n/locales/zh/project.json @@ -0,0 +1,147 @@ +{ + "sidebar": { + "navigationAria": "项目导航", + "brandName": "AGENTHUB", + "workspaceLabel": "项目工作区", + "navOverview": "概览", + "navTasks": "任务", + "navFiles": "文件", + "navNewTask": "新建任务" + }, + "header": { + "searchLabel": "搜索", + "searchAria": "搜索项目", + "searchPlaceholder": "项目、任务、文件...", + "clearSearch": "清除搜索", + "notifications": "通知", + "settings": "设置", + "currentUser": "当前用户" + }, + "hero": { + "eyebrow": "项目详情", + "description": "协调前端预览页面、里程碑、任务就绪状态、设计文件和试运行记录,并清晰展示在线、离线快照和模拟回退状态。", + "simulateSync": "模拟同步", + "syncAgain": "再次同步", + "markRisksReviewed": "标记风险已审查", + "reopenRisks": "重新打开风险", + "newTask": "新建任务", + "deliveryProgress": "交付进度", + "deliveryProgressAria": "交付进度 {{percent}}%", + "openRisks": "未解决风险", + "riskReviewAria": "风险审查进度", + "catalogStatus": "目录状态" + }, + "metrics": { + "aria": "项目指标", + "activeTasks": "进行中任务", + "demoActiveTasks": "演示进行中任务", + "milestones": "里程碑", + "demoMilestones": "演示里程碑", + "sharedFiles": "共享文件", + "demoSharedFiles": "演示共享文件", + "dryRuns": "试运行", + "demoDryRuns": "演示试运行" + }, + "board": { + "tabsAria": "项目看板分区", + "overviewTitle": "项目概览 ({{count}})", + "tasksTitle": "任务状态 ({{count}})", + "filesTitle": "文件与运行记录 ({{files}}/{{runs}})", + "doneTotal": "已完成 {{done}} / 共 {{total}}" + }, + "overview": { + "emptyTitle": "没有匹配的项目。", + "emptyHint": "清除搜索框以恢复概览列表。" + }, + "tasks": { + "newTask": "新建任务", + "emptyTitle": "没有可见的任务。", + "emptyHint": "清除搜索或添加本地任务以重新填充看板。" + }, + "files": { + "label": "文件", + "filterAria": "文件类型筛选", + "emptyTitle": "没有匹配此筛选的文件。", + "emptyHint": "选择\"全部\"或清除搜索以显示项目文件。" + }, + "runs": { + "label": "运行", + "filterAria": "运行状态筛选", + "recordsAria": "运行记录", + "emptyTitle": "没有匹配此筛选的运行记录。", + "emptyHint": "执行本地同步或切换运行状态筛选。" + }, + "milestones": { + "title": "里程碑" + }, + "risks": { + "title": "风险", + "reviewed": "已审查", + "needsReview": "需要审查", + "review": "审查", + "reopen": "重新打开" + }, + "taskForm": { + "drawerAria": "新建任务面板", + "title": "新建任务草稿", + "description": "此面板仅为本地 UI 演示。它展示了项目页面如何在无需连接后端的情况下提供任务创建功能。", + "fieldTitle": "标题", + "fieldOwner": "负责人", + "fieldNote": "备注", + "saveLocal": "本地保存草稿", + "close": "关闭", + "closeAria": "关闭", + "validationRequired": "标题和负责人为必填项。" + }, + "status": { + "done": "已完成", + "active": "进行中", + "next": "待处理", + "inProgress": "进行中", + "pass": "通过", + "ready": "就绪", + "deferred": "已延期", + "local": "本地", + "reviewed": "已审查", + "open": "未解决", + "tracked": "跟踪中", + "edited": "已编辑", + "later": "待定", + "idle": "空闲", + "localSyncComplete": "本地同步完成", + "notSyncedYet": "尚未同步" + }, + "actions": { + "start": "开始", + "markDone": "标记完成", + "reopen": "重新打开" + }, + "filter": { + "all": "全部" + }, + "confirm": { + "saveValidation": "保存前请填写任务标题和负责人。", + "taskSaved": "已将\"{{title}}\"保存为本地任务。", + "taskMoved": "\"{{title}}\" 已移至 {{status}}。", + "riskReviewed": "\"{{title}}\" 已标记为已审查。", + "riskReopened": "\"{{title}}\" 已重新打开以供审查。", + "allRisksReviewed": "所有可审查的风险均已标记为已审查。", + "allRisksReopened": "可审查的风险已重新打开。", + "syncComplete": "同步已于 {{time}} 更新本地运行记录。", + "fallbackNote": "未添加附加备注。", + "openRisksRemaining": "{{count}} 项未解决风险仍需审查。", + "activeTasksRemaining": "{{count}} 项进行中任务经本地审查后仍保留。", + "localSyncDetail": "本地同步捕获了 {{tasks}} 项进行中任务和 {{risks}} 项未解决风险。" + }, + "source": { + "edgeSnapshot": "Edge 快照", + "offlineSnapshot": "离线快照", + "loadingSnapshot": "正在加载快照", + "mockFallback": "演示 / mock fallback", + "snapshotUnavailable": "快照不可用", + "localDryRun": "本地 dry-run / {{source}}" + }, + "demo": { + "cardDetail": "演示/mock fallback 数据:{{detail}}" + } +} diff --git a/app/web/src/i18n/locales/zh/status.json b/app/web/src/i18n/locales/zh/status.json new file mode 100644 index 00000000..19524056 --- /dev/null +++ b/app/web/src/i18n/locales/zh/status.json @@ -0,0 +1,40 @@ +{ + "status.online": "本地 Edge:在线 - {{version}}({{edgeId}})", + "status.offline": "本地 Edge:离线", + "status.wsConnected": "WS:已连接", + "status.wsDisconnected": "WS:已断开", + "agent.title": "智能体", + "agent.emptyOnline": "当前没有可用的智能体", + "agent.emptyOffline": "正在等待 Edge...", + "agent.capability.streaming": "流式输出", + "agent.capability.toolCalls": "工具调用", + "agent.capability.fileChanges": "文件变更", + "agent.capability.thinking": "思考中", + "agent.capability.multiTurn": "多轮对话", + "thread.title": "线程", + "thread.empty": "还没有线程", + "thread.create": "新建线程", + "thread.search": "搜索线程...", + "chat.empty": "选择一个线程并发送消息开始", + "chat.thinking": "思考中...", + "chat.toolCall": "工具:{{name}}", + "chat.fileChange": "已变更:{{path}}", + "chat.sessionInit": "会话已初始化 - {{model}}", + "chat.result.success": "已完成 - 输入 {{input}} / 输出 {{output}} Tokens", + "chat.result.failed": "失败:{{error}}", + "event.title": "事件", + "event.emptyOnline": "正在等待事件...", + "event.emptyOffline": "启动 Edge Server 后接收事件", + "prompt.placeholder": "输入消息,@Agent 可提及智能体...", + "prompt.agentSelector": "选择智能体", + "run.status.queued": "已排队", + "run.status.running": "运行中", + "run.status.finished": "已完成", + "run.status.failed": "失败", + "run.status.idle": "空闲", + "run.title": "运行详情", + "run.output": "输出", + "run.toolCalls": "工具调用", + "run.fileChanges": "变更文件", + "error.streamError": "事件流错误:{{message}}" +} diff --git a/app/web/src/i18n/locales/zh/workbench.json b/app/web/src/i18n/locales/zh/workbench.json new file mode 100644 index 00000000..f660da8e --- /dev/null +++ b/app/web/src/i18n/locales/zh/workbench.json @@ -0,0 +1,162 @@ +{ + "status": { + "connected": "已连接", + "disconnected": "已断开", + "idle": "空闲", + "error": "错误", + "online": "在线", + "offline": "离线", + "active": "活跃", + "queued": "已排队", + "running": "运行中", + "finished": "已完成", + "failed": "失败", + "pending": "待定", + "approved": "已批准", + "rejected": "已拒绝", + "loading": "加载中", + "ready": "就绪", + "waiting_approval": "等待审批" + }, + "leftRail": { + "ariaLabel": "项目与线程导航", + "workbench": "工作台", + "project": "项目", + "noProject": "未加载项目", + "connectEdge": "连接 Edge 以加载项目", + "threads": "线程", + "noThreads": "Edge 未返回任何线程。", + "runners": "Runner 状态", + "noRunners": "没有已注册的 Runner。" + }, + "runner": { + "adapterReady": "适配器就绪" + }, + "header": { + "thread": "线程", + "noThread": "未选择线程", + "edgeUnavailable": "Edge 不可用。快照数据(如已加载)已保留。", + "subtitle": "项目、消息、运行进度、审批和产物在此统一审查路径中。", + "runStatus": "Run {{status}}", + "startRun": "启动 Run", + "starting": "启动中" + }, + "runSummary": { + "ariaLabel": "Run 摘要", + "noRun": "无 Run", + "artifacts": "产物", + "approvalGate": "审批关", + "noApproval": "无审批", + "previewTarget": "预览目标", + "noPreview": "无预览" + }, + "messages": { + "ariaLabel": "线程消息与 Run 时间线", + "title": "IM 消息流", + "subtitle": "@Agent 协作", + "empty": "此线程未加载消息。" + }, + "message": { + "author": { + "owner": "Owner", + "agent": "Agent" + }, + "empty": "(空消息)" + }, + "timeline": { + "ariaLabel": "AgentRun 时间线", + "title": "AgentRun 时间线", + "queuedDone": "线程已接受 Owner 请求。", + "queuedWaiting": "等待 Run。", + "startedWaiting": "Runner 尚未启动。", + "logsAvailable": "日志可在工作区面板中查看。", + "noLogs": "暂无日志。", + "artifactsAvailable": "产物可供审查。", + "noArtifacts": "暂无产物。" + }, + "approval": { + "ariaLabel": "审批请求", + "title": "审批请求", + "empty": "无审批请求", + "description": "审批决定将发送至 Edge。界面仅在 API 调用成功后更新。", + "noPending": "Edge 尚未返回此 Run 的待定审批。", + "reject": "拒绝", + "approve": "批准", + "status": { + "approved": "已批准", + "rejected": "已拒绝", + "pending": "待审批" + } + }, + "composer": { + "attach": "附加上下文", + "threadAriaLabel": "消息线程", + "placeholder": "向此线程发送消息 @ClaudeCode / @Codex / @OpenCode...", + "sending": "发送中", + "send": "发送", + "lockedPlaceholder": "连接 Edge 并加载快照后才能发送" + }, + "workspace": { + "ariaLabel": "工作台工作区", + "title": "工作区", + "subtitle": "文件、Diff、预览、日志、审批", + "tabs": { + "ariaLabel": "工作区面板", + "files": "已更改文件", + "diff": "Diff", + "preview": "预览", + "logs": "日志", + "approval": "审批" + }, + "summary": { + "ariaLabel": "工作区摘要", + "artifacts": "产物", + "artifactCount": "{{count}} 个文件", + "risk": "风险", + "approvalNeeded": "需要审批", + "noPendingGate": "无待定关卡" + }, + "files": { + "fileInfo": "{{size}} KB - 创建于 {{date}}" + }, + "diff": { + "ariaLabel": "产物 Diff 摘要" + }, + "preview": { + "unavailable": "预览不可用", + "ready": "预览就绪", + "notReady": "无可用预览", + "hint": "在 Run 产生可审查输出后创建预览。", + "request": "请求预览", + "open": "打开预览" + }, + "logs": { + "ariaLabel": "Run 日志" + }, + "approval": { + "artifacts": "产物", + "artifactsAvailable": "产物可供审查。", + "noArtifacts": "未加载产物。", + "applyGate": "应用关卡", + "ownerApprovalRequired": "需要 Owner 审批才能应用/丢弃。", + "noPendingRequest": "没有待处理的审批请求。", + "decisionSource": "决策状态来自 Edge API 或 WebSocket 事件。" + }, + "empty": { + "artifacts": "此 Run 未返回产物。", + "diffArtifacts": "此 Run 尚未创建产物。", + "logs": "Edge 未返回 Run 日志。" + } + }, + "error": { + "edgeUnavailable": "Edge 不可用", + "edgeApiTimeout": "Edge API 无响应,请检查 Edge 是否运行在 127.0.0.1:3210。", + "edgeEventStream": "Edge 事件流错误" + }, + "state": { + "loading": "正在加载 Edge 快照。Reducer 拿到真实数据前,写入操作会保持锁定。", + "offlineSnapshot": "Edge 已离线。当前显示最后一次 reducer 快照,写入操作已锁定。", + "offlineEmpty": "Edge 不可用,且尚未加载任何快照。页面不会展示伪造的成功状态。", + "empty": "尚无 Edge 快照数据。请启动 Edge 以加载项目、线程、Runner 和 Run。" + } +} diff --git a/app/web/src/pages/AgentSquare.tsx b/app/web/src/pages/AgentSquare.tsx index 0a4c09bf..32758350 100644 --- a/app/web/src/pages/AgentSquare.tsx +++ b/app/web/src/pages/AgentSquare.tsx @@ -1 +1 @@ -export { default } from './agent-square/AgentSquarePage'; +export { default } from '@/pages/agent-square/AgentSquarePage'; diff --git a/app/web/src/pages/GroupWorkspace.tsx b/app/web/src/pages/GroupWorkspace.tsx index 9a2e46d8..e5512aed 100644 --- a/app/web/src/pages/GroupWorkspace.tsx +++ b/app/web/src/pages/GroupWorkspace.tsx @@ -1 +1 @@ -export { default } from './group-workspace/GroupWorkspacePage'; +export { default } from '@/pages/group-workspace/GroupWorkspacePage'; diff --git a/app/web/src/pages/PrivateChats.tsx b/app/web/src/pages/PrivateChats.tsx index a0eb6319..2a6d9582 100644 --- a/app/web/src/pages/PrivateChats.tsx +++ b/app/web/src/pages/PrivateChats.tsx @@ -1 +1 @@ -export { default } from './private-chats/PrivateChatsPage'; +export { default } from '@/pages/private-chats/PrivateChatsPage'; diff --git a/app/web/src/pages/Project.tsx b/app/web/src/pages/Project.tsx index 121afa45..e61a7322 100644 --- a/app/web/src/pages/Project.tsx +++ b/app/web/src/pages/Project.tsx @@ -1 +1 @@ -export { default } from './projects/ProjectPage'; +export { default } from '@/pages/projects/ProjectPage'; diff --git a/app/web/src/pages/Workbench.tsx b/app/web/src/pages/Workbench.tsx index 8d4eac60..f1c5768c 100644 --- a/app/web/src/pages/Workbench.tsx +++ b/app/web/src/pages/Workbench.tsx @@ -1 +1 @@ -export { default } from './workbench/WorkbenchPage'; +export { default } from '@/pages/workbench/WorkbenchPage'; diff --git a/app/web/src/pages/agent-square/AgentSquarePage.module.css b/app/web/src/pages/agent-square/AgentSquarePage.module.css index adc40832..210e46c6 100644 --- a/app/web/src/pages/agent-square/AgentSquarePage.module.css +++ b/app/web/src/pages/agent-square/AgentSquarePage.module.css @@ -1,225 +1,361 @@ -/* AgentSquarePage — Agent marketplace styles */ -.pageRoot { position: relative; min-height: 100vh; } +/* ═════════════════════════════════════════════════════════════════════ + AgentSquarePage — Agent marketplace with search, filters, card grid + Uses CSS tokens exclusively: no hardcoded hex / px font sizes + ═════════════════════════════════════════════════════════════════════ */ -/* Stats row */ -.stats { - display: grid; - grid-template-columns: repeat(4, minmax(120px, 1fr)); - gap: 10px; - margin-bottom: 16px; -} - -.statCard { +/* ── Root ───────────────────────────────────────────────────────────── */ +.root { display: flex; - align-items: center; - gap: 12px; - padding: 14px; - border: 1px solid var(--border); - border-radius: 12px; - background: rgba(255, 255, 255, 0.52); + flex-direction: column; + flex: 1; + min-height: 0; + overflow: hidden; } -.statIcon { - width: 36px; - height: 36px; +/* ── Top Bar ────────────────────────────────────────────────────────── */ +.topBar { display: flex; - align-items: center; - justify-content: center; - border-radius: 10px; - background: rgba(23, 105, 232, 0.1); - color: var(--primary); + flex-direction: column; + gap: var(--space-lg); + padding: var(--space-xl); + background: var(--card); + border-bottom: 1px solid var(--border); flex-shrink: 0; } -.statIconCyan { color: #087f9e; background: rgba(8, 167, 207, 0.11); } -.statIconPurple { color: #6044d7; background: rgba(116, 87, 232, 0.11); } -.statIconGreen { color: #15744b; background: rgba(29, 155, 103, 0.11); } +.pageTitle { + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + color: var(--foreground); + margin: 0; +} -.statValue { display: block; font-size: 20px; font-weight: 700; line-height: 1; color: var(--foreground); } -.statLabel { font-size: 12px; color: var(--muted-foreground); margin-top: 2px; } +.pageSubtitle { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-normal); + color: var(--muted-foreground); + margin: var(--space-xs) 0 0; +} -/* Market section */ -.market { +/* ── Search ─────────────────────────────────────────────────────────── */ +.searchBar { display: flex; - flex-direction: column; - gap: 12px; - flex: 1; - min-height: 0; - padding: 16px; + align-items: center; + gap: var(--space-sm); + padding: var(--space-sm) var(--space-md); border: 1px solid var(--border); - border-radius: 12px; - background: rgba(255, 255, 255, 0.72); - backdrop-filter: blur(28px); - overflow: hidden; + border-radius: var(--radius-lg); + background: var(--background); + max-width: 480px; } -.marketHead { +.searchInput { + flex: 1; + min-width: 0; + border: none; + outline: none; + background: transparent; + color: var(--foreground); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-normal); +} + +.searchInput::placeholder { + color: var(--muted-foreground); +} + +.searchIcon { + color: var(--muted-foreground); + flex-shrink: 0; +} + +/* ── Filter Tabs ────────────────────────────────────────────────────── */ +.filterBar { display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; + gap: var(--space-sm); flex-wrap: wrap; } -.marketTitle { font-size: 16px; font-weight: 700; color: var(--foreground); } -.marketSubtitle { font-size: 11px; color: var(--muted-foreground); font-weight: 800; text-transform: uppercase; letter-spacing: 0.09em; } - -.filterSummary { +.filterTab { display: flex; align-items: center; - gap: 8px; - flex-wrap: wrap; + gap: var(--space-xs); + padding: var(--space-xs) var(--space-md); + border: 1px solid var(--border); + border-radius: var(--radius-full); + background: transparent; + color: var(--muted-foreground); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-normal); + cursor: pointer; + transition: + background var(--duration-fast) var(--ease-standard), + color var(--duration-fast) var(--ease-standard), + border-color var(--duration-fast) var(--ease-standard); +} + +.filterTab:hover { + background: var(--secondary); + color: var(--foreground); +} + +.filterTabActive { + background: var(--primary); + color: var(--primary-foreground); + border-color: var(--primary); + font-weight: var(--font-weight-medium); } -/* Agent grid */ -.agentGrid { +.filterCount { + font-size: var(--font-size-xs); + font-weight: var(--font-weight-normal); + opacity: 0.7; +} + +/* ── Agent Grid ─────────────────────────────────────────────────────── */ +.grid { display: grid; - grid-template-columns: repeat(3, minmax(230px, 1fr)); - gap: 14px; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--space-lg); + padding: var(--space-xl); overflow-y: auto; flex: 1; - padding: 2px 4px 6px 2px; + align-content: start; } -/* Agent card */ -.agentCard { +/* ── Agent Card ─────────────────────────────────────────────────────── */ +.card { display: flex; flex-direction: column; - gap: 12px; - padding: 15px; - border: 1px solid rgba(255, 255, 255, 0.7); - border-radius: 12px; - background: rgba(255, 255, 255, 0.72); - box-shadow: 0 18px 48px rgba(26, 40, 80, 0.14); - transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease; + gap: var(--space-md); + padding: var(--space-lg); + border: 1px solid var(--border); + border-radius: var(--radius-xl); + background: var(--card); + box-shadow: var(--shadow-sm); + transition: + border-color var(--duration-fast) var(--ease-standard), + box-shadow var(--duration-fast) var(--ease-standard), + transform var(--duration-fast) var(--ease-standard); } -.agentCard:hover { - transform: translateY(-2px); - border-color: color-mix(in oklch, var(--primary) 24%, transparent); - box-shadow: 0 22px 54px rgba(26, 40, 80, 0.16); +.card:hover { + border-color: var(--ring); + box-shadow: var(--shadow-md); + transform: translateY(-1px); } -.agentCardInstalled { border-color: rgba(29, 155, 103, 0.28); } +/* ── Card Header (avatar + name + status) ───────────────────────────── */ +.cardHeader { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--space-sm); +} -.cardHead { display: flex; align-items: flex-start; justify-content: space-between; } -.cardTitle { display: flex; align-items: center; gap: 10px; min-width: 0; } +.cardAvatar { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + flex-shrink: 0; + border-radius: var(--radius-lg); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--primary-foreground); +} -.agentLogo { - width: 40px; height: 40px; - display: flex; align-items: center; justify-content: center; - border-radius: 10px; flex-shrink: 0; - color: var(--primary); - background: rgba(23, 105, 232, 0.1); +.avatarBlue { + background: var(--info); } -.logoBlue { color: var(--primary); background: rgba(23, 105, 232, 0.1); } -.logoCyan { color: #087f9e; background: rgba(8, 167, 207, 0.11); } -.logoPurple { color: #6044d7; background: rgba(116, 87, 232, 0.11); } -.logoGreen { color: #15744b; background: rgba(29, 155, 103, 0.11); } +.avatarGreen { + background: var(--success); +} + +.avatarPurple { + background: var(--brand); +} + +.avatarOrange { + background: var(--warning); +} + +.cardTitle { + flex: 1; + min-width: 0; +} .cardName { - font-size: 13px; font-weight: 700; color: var(--foreground); - overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--foreground); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.cardCategory { font-size: 11px; color: var(--muted-foreground); } +.cardCategory { + font-size: var(--font-size-xs); + font-weight: var(--font-weight-normal); + color: var(--muted-foreground); + margin-top: var(--space-xs); +} -.favBtn { - width: 34px; height: 34px; - display: flex; align-items: center; justify-content: center; - border: 1px solid rgba(23, 105, 232, 0.14); border-radius: 8px; - background: rgba(255, 255, 255, 0.62); color: var(--foreground); - cursor: pointer; flex-shrink: 0; padding: 0; +/* ── Status Dot ─────────────────────────────────────────────────────── */ +.statusDot { + width: 10px; + height: 10px; + border-radius: var(--radius-full); + flex-shrink: 0; } -.favBtnActive { - color: #fff; border-color: transparent; - background: var(--primary); +.dotOnline { + background: var(--success); + box-shadow: 0 0 0 4px color-mix(in oklch, var(--success) 12%, transparent); } -.cardCopy { font-size: 13px; line-height: 1.45; color: var(--muted-foreground); min-height: 58px; } +.dotBusy { + background: var(--warning); + box-shadow: 0 0 0 4px color-mix(in oklch, var(--warning) 12%, transparent); +} + +.dotOffline { + background: var(--muted-foreground); + box-shadow: 0 0 0 4px color-mix(in oklch, var(--muted-foreground) 12%, transparent); +} + +/* ── Description ────────────────────────────────────────────────────── */ +.cardDesc { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-normal); + color: var(--muted-foreground); + line-height: var(--line-height-normal); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} -.tagRow { display: flex; flex-wrap: wrap; gap: 6px; } +/* ── Tags ───────────────────────────────────────────────────────────── */ +.tags { + display: flex; + flex-wrap: wrap; + gap: var(--space-xs); +} .tag { - font-size: 11px; font-weight: 800; padding: 5px 9px; - border: 1px solid rgba(23, 105, 232, 0.13); border-radius: 999px; - background: rgba(23, 105, 232, 0.08); color: #1459c7; + font-size: var(--font-size-xs); + font-weight: var(--font-weight-normal); + padding: var(--space-xs) var(--space-sm); + border: 1px solid var(--border); + border-radius: var(--radius-full); + background: var(--secondary); + color: var(--secondary-foreground); white-space: nowrap; } -.tagCyan { border-color: rgba(8, 167, 207, 0.18); background: rgba(8, 167, 207, 0.1); color: #087f9e; } -.tagPurple { border-color: rgba(116, 87, 232, 0.18); background: rgba(116, 87, 232, 0.1); color: #6044d7; } -.tagGreen { border-color: rgba(29, 155, 103, 0.2); background: rgba(29, 155, 103, 0.11); color: #15744b; } +/* ── Card Actions ───────────────────────────────────────────────────── */ +.cardActions { + display: flex; + gap: var(--space-sm); + margin-top: auto; +} -.cardMeta { display: flex; justify-content: space-between; font-size: 12px; color: var(--muted-foreground); } +.cardBtn { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-xs); + padding: var(--space-sm) var(--space-md); + border: 1px solid var(--border); + border-radius: var(--radius-md); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-normal); + cursor: pointer; + transition: + background var(--duration-fast) var(--ease-standard), + color var(--duration-fast) var(--ease-standard), + border-color var(--duration-fast) var(--ease-standard); + flex: 1; +} -.cardActions { display: flex; justify-content: space-between; gap: 8px; margin-top: auto; } -.cardActions > * { flex: 1; } +.btnView { + background: transparent; + color: var(--foreground); +} -/* Empty state */ -.emptyState { - display: flex; flex-direction: column; align-items: center; justify-content: center; - grid-column: 1 / -1; gap: 12px; padding: 28px; - border: 1px dashed rgba(23, 105, 232, 0.22); border-radius: 12px; - background: rgba(255, 255, 255, 0.42); text-align: center; +.btnView:hover { + background: var(--secondary); } -.emptyTitle { font-size: 15px; font-weight: 700; color: var(--foreground); } -.emptyHint { font-size: 12px; color: var(--muted-foreground); } +.btnDeploy { + background: var(--primary); + color: var(--primary-foreground); + border-color: var(--primary); +} -/* Drawer styles */ -.drawerHead { display: flex; justify-content: space-between; gap: 12px; padding-bottom: 14px; border-bottom: 1px solid var(--border); } -.drawerTitle { font-size: 16px; font-weight: 700; color: var(--foreground); } -.drawerSubtitle { font-size: 11px; color: var(--muted-foreground); font-weight: 800; text-transform: uppercase; letter-spacing: 0.09em; } +.btnDeploy:hover { + opacity: 0.9; +} -.drawerSection { display: flex; flex-direction: column; gap: 10px; } -.drawerSectionTitle { font-size: 13px; font-weight: 700; color: var(--foreground); } +/* ── Stats ──────────────────────────────────────────────────────────── */ +.cardStats { + display: flex; + justify-content: space-between; + font-size: var(--font-size-xs); + font-weight: var(--font-weight-normal); + color: var(--muted-foreground); +} -.drawerHero { display: flex; align-items: center; gap: 12px; } -.drawerHeroLogo { width: 44px; height: 44px; } +/* ── Empty State ────────────────────────────────────────────────────── */ +.emptyState { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + grid-column: 1 / -1; + padding: var(--space-3xl); + text-align: center; + gap: var(--space-md); +} -.drawerDesc { font-size: 12px; line-height: 1.45; color: var(--muted-foreground); } +.emptyIcon { + color: var(--muted-foreground); + opacity: 0.4; +} -.drawerRow { display: flex; align-items: center; gap: 12px; padding: 10px; border: 1px solid rgba(255, 255, 255, 0.62); border-radius: 12px; background: rgba(255, 255, 255, 0.46); } -.drawerRowIcon { width: 34px; height: 34px; } +.emptyTitle { + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + color: var(--foreground); +} -.drawerEmpty { - display: flex; flex-direction: column; align-items: center; justify-content: center; - flex: 1; gap: 12px; text-align: center; - border: 1px dashed rgba(23, 105, 232, 0.22); border-radius: 12px; - background: rgba(255, 255, 255, 0.42); padding: 28px; +.emptyText { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-normal); + color: var(--muted-foreground); } -/* Confirmation bar */ -.confirmBar { - position: fixed; left: 50%; bottom: 22px; z-index: 10; - display: flex; align-items: center; gap: 12px; - width: min(560px, calc(100vw - 44px)); - padding: 12px 14px; transform: translateX(-50%); - border: 1px solid rgba(255, 255, 255, 0.7); border-radius: 12px; - background: rgba(255, 255, 255, 0.72); - box-shadow: 0 18px 48px rgba(26, 40, 80, 0.14); - backdrop-filter: blur(28px); +/* ── Responsive ─────────────────────────────────────────────────────── */ +@media (max-width: 1100px) { + .grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } } -.confirmText { flex: 1; min-width: 0; overflow: hidden; } -.confirmTitle { font-size: 12px; font-weight: 700; display: block; } -.confirmDetail { font-size: 11px; color: var(--muted-foreground); } +@media (max-width: 700px) { + .topBar { + padding: var(--space-lg); + } -/* Sort select */ -.sortSelect { - min-height: 38px; padding: 8px 34px 8px 11px; - border: 1px solid rgba(255, 255, 255, 0.68); border-radius: 8px; - background: rgba(255, 255, 255, 0.62); color: var(--foreground); - font-size: 12px; font-weight: 800; cursor: pointer; outline: none; -} + .grid { + grid-template-columns: 1fr; + padding: var(--space-lg); + } -/* Responsive */ -@media (max-width: 1180px) { - .agentGrid { grid-template-columns: repeat(2, minmax(230px, 1fr)); } -} -@media (max-width: 820px) { - .stats, .agentGrid { grid-template-columns: 1fr; } + .searchBar { + max-width: 100%; + } } diff --git a/app/web/src/pages/agent-square/AgentSquarePage.tsx b/app/web/src/pages/agent-square/AgentSquarePage.tsx index 2ba47ab7..90793108 100644 --- a/app/web/src/pages/agent-square/AgentSquarePage.tsx +++ b/app/web/src/pages/agent-square/AgentSquarePage.tsx @@ -1,19 +1,13 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Icon, Button, Pill, SearchInput, ProgressBar } from '@shared/ui'; -import { ParticleCanvas } from '@/components/ParticleCanvas'; -import { WebLayout } from '@/components/WebLayout'; -import styles from './AgentSquarePage.module.css'; - -/* ---- inline mock data (static prototype) ---- */ -const mockRunners: Array<{ id: string; name: string; status: string; capabilities?: string }> = [ - { id: 'runner-001', name: 'TS Builder', status: 'online', capabilities: 'TypeScript,React,Frontend' }, - { id: 'runner-002', name: 'Go Ops Agent', status: 'offline', capabilities: 'Go,Infrastructure,Deploy' }, - { id: 'runner-003', name: 'Python ML Runner', status: 'online', capabilities: 'Python,ML,Data' }, -]; +import { mockRunners } from '@shared/index'; +import { useHubCustomAgents, type HubCustomAgent } from '../../hooks/useHubCustomAgents'; +import { useHubSession } from '../../hooks/useHubSession'; type AgentCategory = 'Engineering' | 'Design' | 'Operations' | 'Research'; + type SortMode = 'popular' | 'rating' | 'recent'; + type ViewMode = 'all' | 'favorites' | 'installed'; type Agent = { @@ -39,19 +33,111 @@ type Confirmation = { title: string; }; -const agents: Agent[] = [ - { id: 'refactor', name: 'Code Refactor Pro', category: 'Engineering', icon: 'code_blocks', tone: 'blue', summary: 'Modernizes front-end and Go service modules while keeping reviewable diffs visible.', description: 'Best for code cleanup passes where the workspace needs scoped patches, local conventions, and clear validation notes before review.', tags: ['TypeScript', 'Go', 'Review'], installs: 14820, favoriteCount: 1824, rating: 4.9, updatedDaysAgo: 6, outputs: ['Scoped patch plan', 'Validation checklist', 'Risk notes'] }, - { id: 'designer', name: 'Interface Critic', category: 'Design', icon: 'palette', tone: 'purple', summary: 'Audits tool surfaces for hierarchy, responsive density, and component states.', description: 'Reviews a workbench page like a product surface: layout rhythm, accessible labels, empty states, and responsive clipping.', tags: ['UI audit', 'A11y', 'Layout'], installs: 9360, favoriteCount: 1211, rating: 4.8, updatedDaysAgo: 3, outputs: ['Visual hierarchy pass', 'A11y notes', 'Responsive risks'] }, - { id: 'qa', name: 'QA Flow Builder', category: 'Engineering', icon: 'fact_check', tone: 'cyan', summary: 'Creates focused checks for UI flows, command output, and API contract edges.', description: 'Useful when a page or workflow needs a compact smoke plan with the right assertions and a clean handoff for testers.', tags: ['Playwright', 'Unit tests', 'Smoke'], installs: 12840, favoriteCount: 1506, rating: 4.7, updatedDaysAgo: 9, outputs: ['Smoke scenarios', 'State matrix', 'Manual QA steps'] }, - { id: 'ops', name: 'Runbook Operator', category: 'Operations', icon: 'terminal', tone: 'green', summary: 'Turns incidents and release notes into commands, checkpoints, and rollback prompts.', description: 'Pairs well with deployment work because it keeps routine operations, checks, and escalation notes in one readable sequence.', tags: ['Runbook', 'Deploy', 'Monitor'], installs: 8700, favoriteCount: 984, rating: 4.6, updatedDaysAgo: 2, outputs: ['Command sequence', 'Rollback prompts', 'Operator handoff'] }, - { id: 'research', name: 'Evidence Synthesizer', category: 'Research', icon: 'travel_explore', tone: 'purple', summary: 'Groups sources into claims, caveats, contradictions, and decision notes.', description: 'Helps teams move from raw references to compact conclusions while preserving the difference between evidence and inference.', tags: ['Sources', 'Citations', 'Summary'], installs: 10320, favoriteCount: 1398, rating: 4.9, updatedDaysAgo: 5, outputs: ['Claim map', 'Source trail', 'Decision summary'] }, - { id: 'release', name: 'Release Steward', category: 'Operations', icon: 'rocket_launch', tone: 'blue', summary: 'Collects branch status, validation commands, changelog points, and merge readiness.', description: 'Keeps release coordination visible across branch status, test evidence, review notes, and handoff copy.', tags: ['PR', 'Validation', 'Changelog'], installs: 7600, favoriteCount: 862, rating: 4.5, updatedDaysAgo: 8, outputs: ['Release checklist', 'Changelog draft', 'Merge summary'] }, +const catalogAgents: Agent[] = [ + { + id: 'refactor', + name: 'Code Refactor Pro', + category: 'Engineering', + icon: 'code_blocks', + tone: 'blue', + summary: 'Modernizes front-end and Go service modules while keeping reviewable diffs visible.', + description: + 'Best for code cleanup passes where the workspace needs scoped patches, local conventions, and clear validation notes before review.', + tags: ['TypeScript', 'Go', 'Review'], + installs: 14820, + favoriteCount: 1824, + rating: 4.9, + updatedDaysAgo: 6, + outputs: ['Scoped patch plan', 'Validation checklist', 'Risk notes'], + }, + { + id: 'designer', + name: 'Interface Critic', + category: 'Design', + icon: 'palette', + tone: 'purple', + summary: 'Audits tool surfaces for hierarchy, responsive density, and component states.', + description: + 'Reviews a workbench page like a product surface: layout rhythm, accessible labels, empty states, and responsive clipping.', + tags: ['UI audit', 'A11y', 'Layout'], + installs: 9360, + favoriteCount: 1211, + rating: 4.8, + updatedDaysAgo: 3, + outputs: ['Visual hierarchy pass', 'A11y notes', 'Responsive risks'], + }, + { + id: 'qa', + name: 'QA Flow Builder', + category: 'Engineering', + icon: 'fact_check', + tone: 'cyan', + summary: 'Creates focused checks for UI flows, command output, and API contract edges.', + description: + 'Useful when a page or workflow needs a compact smoke plan with the right assertions and a clean handoff for testers.', + tags: ['Playwright', 'Unit tests', 'Smoke'], + installs: 12840, + favoriteCount: 1506, + rating: 4.7, + updatedDaysAgo: 9, + outputs: ['Smoke scenarios', 'State matrix', 'Manual QA steps'], + }, + { + id: 'ops', + name: 'Runbook Operator', + category: 'Operations', + icon: 'terminal', + tone: 'green', + summary: 'Turns incidents and release notes into commands, checkpoints, and rollback prompts.', + description: + 'Pairs well with deployment work because it keeps routine operations, checks, and escalation notes in one readable sequence.', + tags: ['Runbook', 'Deploy', 'Monitor'], + installs: 8700, + favoriteCount: 984, + rating: 4.6, + updatedDaysAgo: 2, + outputs: ['Command sequence', 'Rollback prompts', 'Operator handoff'], + }, + { + id: 'research', + name: 'Evidence Synthesizer', + category: 'Research', + icon: 'travel_explore', + tone: 'purple', + summary: 'Groups sources into claims, caveats, contradictions, and decision notes.', + description: + 'Helps teams move from raw references to compact conclusions while preserving the difference between evidence and inference.', + tags: ['Sources', 'Citations', 'Summary'], + installs: 10320, + favoriteCount: 1398, + rating: 4.9, + updatedDaysAgo: 5, + outputs: ['Claim map', 'Source trail', 'Decision summary'], + }, + { + id: 'release', + name: 'Release Steward', + category: 'Operations', + icon: 'rocket_launch', + tone: 'blue', + summary: 'Collects branch status, validation commands, changelog points, and merge readiness.', + description: + 'Keeps release coordination visible across branch status, test evidence, review notes, and handoff copy.', + tags: ['PR', 'Validation', 'Changelog'], + installs: 7600, + favoriteCount: 862, + rating: 4.5, + updatedDaysAgo: 8, + outputs: ['Release checklist', 'Changelog draft', 'Merge summary'], + }, ...mockRunners.map((runner) => ({ - id: runner.id, name: runner.name, category: 'Engineering' as AgentCategory, + id: runner.id, + name: runner.name, + category: 'Engineering' as AgentCategory, icon: runner.status === 'online' ? 'memory' : 'cloud_off', tone: (runner.status === 'online' ? 'cyan' : 'purple') as Agent['tone'], summary: runner.capabilities ?? 'Local runner agent', - description: `Runner ${runner.name} — status: ${runner.status}. Capabilities: ${runner.capabilities ?? 'unknown'}.`, + description: `Runner ${runner.name} - status: ${runner.status}. Capabilities: ${runner.capabilities ?? 'unknown'}.`, tags: [runner.status, ...(runner.capabilities?.split(',') ?? [])], installs: runner.status === 'online' ? 5200 : 800, favoriteCount: runner.status === 'online' ? 340 : 45, @@ -62,16 +148,163 @@ const agents: Agent[] = [ ]; const categories: Array = ['All', 'Engineering', 'Design', 'Operations', 'Research']; + const initialFavoriteIds = new Set(['refactor', mockRunners[0]?.id].filter(Boolean) as string[]); const initialInstalledIds = new Set(['refactor', mockRunners[0]?.id, mockRunners[1]?.id].filter(Boolean) as string[]); const workspaceLimit = 8; -function formatInstalls(installs: number): string { return `${(installs / 1000).toFixed(1)}k`; } +function parseJsonStringArray(value: string | undefined): string[] { + if (!value) return []; + + try { + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === 'string') : []; + } catch { + return []; + } +} + +function agentFromHub(agent: HubCustomAgent, index: number): Agent { + const tags = parseJsonStringArray(agent.capability_tags); + const tone = (['blue', 'cyan', 'purple', 'green'] as const)[index % 4] ?? 'blue'; + + return { + id: agent.id, + name: agent.name, + category: 'Engineering', + icon: agent.avatar_url ? 'account_circle' : 'smart_toy', + tone, + summary: agent.system_prompt || `${agent.agent_type} custom agent`, + description: agent.system_prompt || `${agent.name} is loaded from Hub /web/custom-agents.`, + tags: tags.length ? tags : [agent.agent_type], + installs: 0, + favoriteCount: 0, + rating: 0, + updatedDaysAgo: 0, + outputs: ['Hub custom agent', 'Session mention target', 'Shared profile contract'], + }; +} + +function formatInstalls(installs: number): string { + return `${(installs / 1000).toFixed(1)}k`; +} + +function cx(...classes: Array): string { + return classes.filter(Boolean).join(' '); +} + +function ParticleCanvas() { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + const context = canvas?.getContext('2d'); + + if (!canvas || !context) { + return; + } + + type Particle = { + alpha: number; + hue: number; + radius: number; + velocityX: number; + velocityY: number; + x: number; + y: number; + }; + + let animationFrame = 0; + let height = 0; + let width = 0; + let particles: Particle[] = []; + + const createParticle = (index: number): Particle => ({ + alpha: 0.18 + Math.random() * 0.2, + hue: index % 3 === 0 ? 196 : 210, + radius: 1.6 + Math.random() * 2.6, + velocityX: -0.18 + Math.random() * 0.36, + velocityY: -0.18 - Math.random() * 0.48, + x: Math.random() * width, + y: Math.random() * height, + }); + + const resize = () => { + const ratio = window.devicePixelRatio || 1; + width = window.innerWidth; + height = window.innerHeight; + canvas.width = Math.floor(width * ratio); + canvas.height = Math.floor(height * ratio); + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + context.setTransform(ratio, 0, 0, ratio, 0, 0); + particles = Array.from({ length: 56 }, (_, index) => createParticle(index)); + }; + + const draw = () => { + animationFrame = window.requestAnimationFrame(draw); + context.clearRect(0, 0, width, height); + + particles.forEach((particle, index) => { + particle.x += particle.velocityX; + particle.y += particle.velocityY; -export function AgentSquarePage() { - const { t } = useTranslation(); + if (particle.y < -16) { + particle.y = height + 16; + particle.x = Math.random() * width; + } + + if (particle.x < -16) { + particle.x = width + 16; + } + + if (particle.x > width + 16) { + particle.x = -16; + } + + context.beginPath(); + context.fillStyle = `hsla(${particle.hue}, 84%, 48%, ${particle.alpha})`; + context.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2); + context.fill(); + + for (let nextIndex = index + 1; nextIndex < particles.length; nextIndex += 1) { + const nextParticle = particles[nextIndex]; + if (!nextParticle) { + continue; + } + + const dx = particle.x - nextParticle.x; + const dy = particle.y - nextParticle.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < 126) { + context.beginPath(); + context.strokeStyle = `rgba(23, 105, 232, ${(1 - distance / 126) * 0.07})`; + context.lineWidth = 1; + context.moveTo(particle.x, particle.y); + context.lineTo(nextParticle.x, nextParticle.y); + context.stroke(); + } + } + }); + }; + + resize(); + draw(); + window.addEventListener('resize', resize); + + return () => { + window.cancelAnimationFrame(animationFrame); + window.removeEventListener('resize', resize); + }; + }, []); + + return