foundation: start native language reality engineering root
This commit is contained in:
commit
0dec0b93fc
154 changed files with 49993 additions and 0 deletions
|
|
@ -0,0 +1,51 @@
|
|||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import test from 'node:test'
|
||||
|
||||
const main = await readFile(new URL('../src/main.tsx', import.meta.url), 'utf8')
|
||||
const channel = await readFile(new URL('../src/modules/persona-channel-body/index.tsx', import.meta.url), 'utf8')
|
||||
const runtime = await readFile(new URL('../src-tauri/src/persona_language_channel.rs', import.meta.url), 'utf8')
|
||||
|
||||
test('light lake exposes a first-class human conversation entrance', () => {
|
||||
assert.match(main, /type ViewId = [^\n]*'persona'/)
|
||||
assert.match(main, /knowledge-chat-entry/)
|
||||
assert.match(main, />共同工作</)
|
||||
assert.match(main, /view === 'persona' \? renderPersonaBody\(\)/)
|
||||
assert.match(main, /<PersonaChannelBody/)
|
||||
assert.match(channel, /<textarea[\s\S]*?form\?\.requestSubmit\(\)/)
|
||||
assert.match(channel, /\{sending \? '正在回应…' : '发送'\}/)
|
||||
assert.match(channel, /markdownHtml\(item\.content\)/)
|
||||
assert.match(channel, /persona-message-markdown/)
|
||||
})
|
||||
|
||||
test('knowledge workspace keeps the chat visible and binds the exact active page', () => {
|
||||
assert.match(main, /hololake-knowledge-chat-open'\) !== '0'/)
|
||||
assert.match(main, /knowledge-persona-collaboration/)
|
||||
assert.match(main, /knowledgeFocus=\{activeDocument \? \{ source: activeDocument\.source, path: activeDocument\.path, title: activeDocument\.title, contentSha256: activeDocument\.contentSha256 \}/)
|
||||
assert.match(channel, /activeKnowledgeSource: knowledgeFocus\?\.source/)
|
||||
assert.match(channel, /activeKnowledgePath: knowledgeFocus\?\.path/)
|
||||
assert.match(channel, /引用知识 · \{receipt\.path\} · \{shortHash\(receipt\.contentSha256\)\}/)
|
||||
assert.match(runtime, /collect_knowledge_context\([\s\S]*?input\.active_knowledge_source\.as_deref\(\)[\s\S]*?input\.active_knowledge_path\.as_deref\(\)/)
|
||||
assert.match(runtime, /append_knowledge_document\(&mut context, &mut seen, document\)/)
|
||||
assert.match(runtime, /let secret = read_secret_bounded\(app, provider_id\)\.await\.ok\(\)/)
|
||||
assert.doesNotMatch(runtime, /fn read_secret\(/)
|
||||
})
|
||||
|
||||
test('model configuration remains secondary to the conversation surface', () => {
|
||||
assert.match(channel, /const \[settingsOpen, setSettingsOpen\] = useState\(false\)/)
|
||||
assert.match(channel, /ZERO CORE · LANGUAGE PERSONA CHANNEL/)
|
||||
assert.match(channel, /persona-chat-status/)
|
||||
assert.match(channel, /\{settingsOpen && <aside className="persona-chat-settings">/)
|
||||
assert.match(channel, /API 密钥 · 只进钥匙串/)
|
||||
})
|
||||
|
||||
test('the chosen model route survives restart and prefers explicit user configuration', () => {
|
||||
assert.match(channel, /const PROVIDER_SELECTION_KEY = 'hololake-persona-provider-id'/)
|
||||
assert.match(channel, /localStorage\.getItem\(PROVIDER_SELECTION_KEY\)/)
|
||||
assert.match(channel, /item\.source === 'EXPLICIT_USER_CONFIG'/)
|
||||
assert.match(channel, /rememberProvider\(nextProviderId\)/)
|
||||
assert.match(channel, /onChange=\{\(event\) => selectProvider\(event\.target\.value\)\}/)
|
||||
assert.match(channel, /selectedProvider\?\.source === 'EXPLICIT_USER_CONFIG'/)
|
||||
assert.match(channel, /providerId: existingProviderId/)
|
||||
assert.match(channel, /setModel\(saved\?\.selectedModel \|\| configModel\.trim\(\)\)/)
|
||||
})
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const frontend = readFileSync(new URL('../src/modules/persona-channel-body/index.tsx', import.meta.url), 'utf8')
|
||||
const main = readFileSync(new URL('../src/main.tsx', import.meta.url), 'utf8')
|
||||
const runtime = readFileSync(new URL('../src-tauri/src/persona_language_channel.rs', import.meta.url), 'utf8')
|
||||
const routes = JSON.parse(readFileSync(new URL('../contracts/numbered-ipc-registry.json', import.meta.url), 'utf8'))
|
||||
|
||||
test('human conversations are real numbered entities with create switch and delete controls', () => {
|
||||
for (const alias of ['list_conversations', 'create_conversation', 'delete_conversation']) {
|
||||
assert.match(frontend, new RegExp(`invoke<[^>]+>\\('${alias}'`))
|
||||
assert.ok(routes.operations.some((route) => route.alias === alias && route.module_number === 'HLP-NIPC-MOD-0035'))
|
||||
}
|
||||
assert.match(frontend, /conversationId,\s*providerId/)
|
||||
assert.match(frontend, /setConversationId\(item\.conversationId\)/)
|
||||
assert.match(frontend, /删除对话/)
|
||||
assert.match(main, /setPersonaNewConversationRequest\(\(value\) => value \+ 1\)/)
|
||||
assert.match(main, /newConversationRequest=\{personaNewConversationRequest\}/)
|
||||
})
|
||||
|
||||
test('legacy history migrates without loss and deletion preserves the local receipt chain', () => {
|
||||
assert.match(runtime, /ALTER TABLE messages ADD COLUMN conversation_id/)
|
||||
assert.match(runtime, /migrate_legacy_conversations/)
|
||||
assert.match(runtime, /HLP-CONV-LEGACY-/)
|
||||
assert.match(runtime, /SOFT_DELETE_TOMBSTONE_MESSAGES_RETAINED_FOR_LOCAL_RECEIPT_CHAIN/)
|
||||
assert.match(runtime, /legacy_messages_migrate_into_one_numbered_conversation_without_loss/)
|
||||
assert.match(runtime, /conversations_are_isolated_and_delete_keeps_local_receipt_history/)
|
||||
})
|
||||
|
||||
test('model receives machine environment facts instead of a hard-coded unknown response', () => {
|
||||
assert.match(runtime, /\[ZERO_CORE_CHANNEL_RUNTIME\]/)
|
||||
assert.match(runtime, /human_anchor=\{human_name\} · \{human_id\}/)
|
||||
assert.match(runtime, /知识库工具仍然可用/)
|
||||
assert.match(runtime, /hololake_search_knowledge/)
|
||||
assert.match(runtime, /hololake_read_knowledge_document/)
|
||||
assert.doesNotMatch(runtime, /没有证据就明确说不知道/)
|
||||
assert.match(runtime, /REGISTERED_AGE_PERSONA_BRAIN_DRIVES_REPLACEABLE_MODEL_AND_CODEX_LIMBS/)
|
||||
})
|
||||
|
||||
test('persona tool forge compiles typed HLDP through its own numbered preview route', () => {
|
||||
assert.match(frontend, /preview_hldp_tool_program/)
|
||||
assert.match(frontend, /HLDP NATIVE TOOL FORGE/)
|
||||
assert.match(frontend, /确定性 GIR/)
|
||||
assert.ok(routes.operations.some((route) => route.alias === 'preview_hldp_tool_program'
|
||||
&& route.operation_number === 'HLP-NIPC-OP-0168'
|
||||
&& route.module_number === 'HLP-NIPC-MOD-0035'))
|
||||
assert.match(runtime, /compile_hldp_program\(input\)/)
|
||||
})
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,452 @@
|
|||
import { numberedInvoke as invoke } from '../numbered-ipc'
|
||||
import { markdownHtml } from '../knowledge-render'
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||||
import { type FormEvent, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import './styles.css'
|
||||
|
||||
export interface KnowledgeFocus {
|
||||
source: 'native' | 'legacy'
|
||||
path: string
|
||||
title: string
|
||||
contentSha256: string
|
||||
}
|
||||
|
||||
interface PersonaSummary { personaId: string; displayName: string; state: string }
|
||||
interface PersonaBodySnapshot { state: string; personas: PersonaSummary[]; integrityState: string }
|
||||
interface ModelProvider { providerId: string; label: string; kind: string; state: string; source: string; models: string[]; selectedModel?: string; credentialState: string; detail: string }
|
||||
interface ProviderDiscovery { state: string; providers: ModelProvider[]; secretPolicy: string }
|
||||
interface ToolReceipt { source: string; path: string; contentSha256: string }
|
||||
interface PersonaMessage { messageId: string; eventId: string; personaId: string; role: 'human' | 'persona'; content: string; providerId: string; model: string; createdAtUnixMs: number; stateVersion: number; receiptHash: string; toolReceipts: ToolReceipt[] }
|
||||
interface ConversationSnapshot { personaId: string; conversationId: string; title: string; messages: PersonaMessage[]; stateVersion: number; lastReceiptHash: string }
|
||||
interface ConversationSummary { conversationId: string; personaId: string; title: string; updatedAtUnixMs: number; messageCount: number; lastMessagePreview: string }
|
||||
interface ConversationListSnapshot { personaId: string; conversations: ConversationSummary[]; deletionPolicy: string }
|
||||
interface LimbSnapshot { state: string; limbId: string; executableState: string; executableVersion?: string; trainingCycleCount: number; validatedMemoryCount: number; ledgerIntegrity: string; lastTraining?: { state: string; lessonId: string; summary: string; receiptHash: string } }
|
||||
interface DrillReceipt { state: string; summary: string; receiptHash: string; snapshot: LimbSnapshot }
|
||||
interface AgentProgress {
|
||||
requestId: string
|
||||
conversationId: string
|
||||
stage: string
|
||||
label: string
|
||||
state: 'RUNNING' | 'DONE' | 'FAILED'
|
||||
detail?: string
|
||||
completed: number
|
||||
total?: number
|
||||
occurredAtUnixMs: number
|
||||
}
|
||||
interface GirPreview {
|
||||
schema: string
|
||||
programId: string
|
||||
sourceSha256: string
|
||||
permissions: string[]
|
||||
actionGraph: Array<{ ordinal: number; id: string; kind: string; permission: string }>
|
||||
unresolvedNaturalLanguage: string[]
|
||||
unresolvedPermissions: string[]
|
||||
girSha256: string
|
||||
}
|
||||
|
||||
const PROVIDER_SELECTION_KEY = 'hololake-persona-provider-id'
|
||||
const DEFAULT_HLDP_TOOL = JSON.stringify({
|
||||
schema: 'hololake.hldp-native-program/v1',
|
||||
programId: 'HLP-TOOL-KNOWLEDGE-READ-0001',
|
||||
version: '0.1.0',
|
||||
subjectId: 'ICE-P-ZY001',
|
||||
targetId: 'HOLOLAKE-KNOWLEDGE-LOCAL',
|
||||
scope: 'CURRENT_ACCOUNT_KNOWLEDGE',
|
||||
permissions: ['KNOWLEDGE_READ', 'RECEIPT_WRITE'],
|
||||
resources: { cpuUnits: 1, memoryBytes: 16777216, storageBytes: 0, networkAllowed: false },
|
||||
actions: [
|
||||
{ id: 'validate-input', kind: 'VALIDATE', dependsOn: [], inputDigest: '0'.repeat(64), permission: 'KNOWLEDGE_READ' },
|
||||
{ id: 'read-document', kind: 'READ', dependsOn: ['validate-input'], inputDigest: '0'.repeat(64), permission: 'KNOWLEDGE_READ' },
|
||||
{ id: 'write-receipt', kind: 'WRITE_RECEIPT', dependsOn: ['read-document'], inputDigest: '0'.repeat(64), permission: 'RECEIPT_WRITE' },
|
||||
],
|
||||
timeoutMs: 30000,
|
||||
stopAction: 'STOP',
|
||||
cleanupAction: 'CLEANUP',
|
||||
rollbackAction: 'ROLLBACK',
|
||||
receiptKinds: ['COMPILE', 'READ', 'TERMINAL'],
|
||||
}, null, 2)
|
||||
|
||||
function errorText(error: unknown) {
|
||||
const raw = String(error).replace(/^Error:\s*/, '')
|
||||
if (raw.includes('MODEL_SELECTION_REQUIRED')) return '请选择模型;百炼 Token Plan 可使用 qwen3.8-max。'
|
||||
if (raw.includes('MODEL_HTTP_401') || raw.includes('MODEL_HTTP_403')) return '模型平台拒绝了凭据,请重新保存 API 密钥。'
|
||||
if (raw.includes('MODEL_REQUEST_FAILED') || raw.includes('MODEL_CLIENT_FAILED')) return '没有连接到模型平台,请检查网络与接口地址。'
|
||||
if (raw.includes('TOOL_ROUND_LIMIT')) return '工具调用达到本轮上限,已安全停止。'
|
||||
if (raw.includes('GLC_') || raw.includes('HLDP_PREVIEW')) return 'HLDP 程序没有通过严格类型检查,请检查字段、权限、依赖和摘要。'
|
||||
if (raw.includes('KNOWLEDGE')) return '知识库工具没有完成这一步,失败结果已保留。'
|
||||
return '本轮没有完成。技术详情已保存在本机运行回执中。'
|
||||
}
|
||||
|
||||
function shortHash(value: string) {
|
||||
return value.length > 18 ? `${value.slice(0, 9)}…${value.slice(-6)}` : value
|
||||
}
|
||||
|
||||
function localTime(value: number) {
|
||||
return new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit' }).format(new Date(value))
|
||||
}
|
||||
|
||||
function rememberProvider(providerId: string) {
|
||||
if (providerId) localStorage.setItem(PROVIDER_SELECTION_KEY, providerId)
|
||||
else localStorage.removeItem(PROVIDER_SELECTION_KEY)
|
||||
}
|
||||
|
||||
// 本页登记生命周期,不创建人格绑定。
|
||||
export function PersonaChannelBody({ onBack, knowledgeFocus, embedded = false, newConversationRequest = 0 }: { onBack?: () => void; knowledgeFocus?: KnowledgeFocus | null; embedded?: boolean; newConversationRequest?: number }) {
|
||||
const [body, setBody] = useState<PersonaBodySnapshot | null>(null)
|
||||
const [discovery, setDiscovery] = useState<ProviderDiscovery | null>(null)
|
||||
const [conversation, setConversation] = useState<ConversationSnapshot | null>(null)
|
||||
const [conversations, setConversations] = useState<ConversationSummary[]>([])
|
||||
const [conversationId, setConversationId] = useState('')
|
||||
const [personaId, setPersonaId] = useState('')
|
||||
const [providerId, setProviderId] = useState(() => localStorage.getItem(PROVIDER_SELECTION_KEY) || '')
|
||||
const [model, setModel] = useState('')
|
||||
const [draft, setDraft] = useState('')
|
||||
const [notice, setNotice] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [sending, setSending] = useState(false)
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
const [forgeOpen, setForgeOpen] = useState(false)
|
||||
const [personaName, setPersonaName] = useState('铸渊 · 本机频道')
|
||||
const [configLabel, setConfigLabel] = useState('百炼 Token Plan')
|
||||
const [configBaseUrl, setConfigBaseUrl] = useState('https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1')
|
||||
const [configModel, setConfigModel] = useState('qwen3.8-max')
|
||||
const [configKey, setConfigKey] = useState('')
|
||||
const [activity, setActivity] = useState<AgentProgress[]>([])
|
||||
const [hldpProgram, setHldpProgram] = useState(DEFAULT_HLDP_TOOL)
|
||||
const [girPreview, setGirPreview] = useState<GirPreview | null>(null)
|
||||
const [forgeBusy, setForgeBusy] = useState(false)
|
||||
const [limb, setLimb] = useState<LimbSnapshot | null>(null)
|
||||
const [limbBusy, setLimbBusy] = useState(false)
|
||||
const ledgerRef = useRef<HTMLDivElement>(null)
|
||||
const activeRequestRef = useRef('')
|
||||
const handledNewConversationRequest = useRef(0)
|
||||
|
||||
const selectedProvider = useMemo(() => discovery?.providers.find((item) => item.providerId === providerId), [discovery, providerId])
|
||||
const selectedPersona = useMemo(() => body?.personas.find((item) => item.personaId === personaId), [body, personaId])
|
||||
|
||||
const refreshProviders = async () => {
|
||||
const next = await invoke<ProviderDiscovery>('discover_model_providers')
|
||||
setDiscovery(next)
|
||||
setProviderId((current) => {
|
||||
const available = next.providers.filter((item) => item.state === 'AVAILABLE')
|
||||
const stored = localStorage.getItem(PROVIDER_SELECTION_KEY)
|
||||
const selected = available.find((item) => item.providerId === current)
|
||||
|| available.find((item) => item.providerId === stored)
|
||||
|| available.find((item) => item.source === 'EXPLICIT_USER_CONFIG')
|
||||
|| available[0]
|
||||
const nextProviderId = selected?.providerId || ''
|
||||
rememberProvider(nextProviderId)
|
||||
return nextProviderId
|
||||
})
|
||||
}
|
||||
|
||||
const selectProvider = (nextProviderId: string) => {
|
||||
rememberProvider(nextProviderId)
|
||||
setProviderId(nextProviderId)
|
||||
}
|
||||
|
||||
const refreshBody = async () => {
|
||||
const next = await invoke<PersonaBodySnapshot>('get_persona_channel_body')
|
||||
setBody(next)
|
||||
setPersonaId((current) => current && next.personas.some((item) => item.personaId === current) ? current : (next.personas[0]?.personaId || ''))
|
||||
}
|
||||
|
||||
const refreshConversations = async (targetPersonaId: string, preferredId = '') => {
|
||||
const snapshot = await invoke<ConversationListSnapshot>('list_conversations', { input: { personaId: targetPersonaId, conversationId: null } })
|
||||
setConversations(snapshot.conversations)
|
||||
const nextId = snapshot.conversations.some((item) => item.conversationId === preferredId)
|
||||
? preferredId
|
||||
: snapshot.conversations[0]?.conversationId || ''
|
||||
setConversationId(nextId)
|
||||
if (!nextId) setConversation(null)
|
||||
return snapshot
|
||||
}
|
||||
|
||||
const createConversation = async () => {
|
||||
if (!personaId) return
|
||||
setLoading(true)
|
||||
try {
|
||||
const created = await invoke<ConversationSnapshot>('create_conversation', { input: { personaId, title: null } })
|
||||
setConversation(created)
|
||||
await refreshConversations(personaId, created.conversationId)
|
||||
setNotice('新对话已建立;它拥有独立历史和回执坐标。')
|
||||
} catch (error) { setNotice(errorText(error)) }
|
||||
finally { setLoading(false) }
|
||||
}
|
||||
|
||||
const deleteConversation = async (target: ConversationSummary) => {
|
||||
if (!window.confirm(`删除对话“${target.title}”?它会从列表移除,本机回执链保留不可冒充的删除墓碑。`)) return
|
||||
setLoading(true)
|
||||
try {
|
||||
const snapshot = await invoke<ConversationListSnapshot>('delete_conversation', { input: { personaId, conversationId: target.conversationId } })
|
||||
setConversations(snapshot.conversations)
|
||||
const nextId = snapshot.conversations[0]?.conversationId || ''
|
||||
setConversationId(nextId)
|
||||
if (!nextId) setConversation(null)
|
||||
setNotice('对话已从活动列表删除;本机审计回执链保留删除墓碑。')
|
||||
} catch (error) { setNotice(errorText(error)) }
|
||||
finally { setLoading(false) }
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([refreshBody(), refreshProviders(), invoke<LimbSnapshot>('get_execution_limb_snapshot').then(setLimb)])
|
||||
.catch((error) => setNotice(errorText(error)))
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let unlisten: UnlistenFn | undefined
|
||||
void listen<AgentProgress>('hololake://persona-agent-progress', ({ payload }) => {
|
||||
if (!payload || payload.requestId !== activeRequestRef.current) return
|
||||
setActivity((current) => {
|
||||
const index = current.findIndex((item) => item.stage === payload.stage)
|
||||
if (index < 0) return [...current, payload]
|
||||
const next = [...current]
|
||||
next[index] = payload
|
||||
return next
|
||||
})
|
||||
}).then((dispose) => { unlisten = dispose })
|
||||
return () => unlisten?.()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const nextModel = selectedProvider?.selectedModel || selectedProvider?.models[0] || ''
|
||||
setModel(nextModel)
|
||||
}, [selectedProvider])
|
||||
|
||||
useEffect(() => {
|
||||
if (!personaId) { setConversation(null); setConversations([]); setConversationId(''); return }
|
||||
refreshConversations(personaId, conversationId)
|
||||
.catch((error) => setNotice(errorText(error)))
|
||||
}, [personaId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!personaId || !conversationId) { setConversation(null); return }
|
||||
invoke<ConversationSnapshot>('get_conversation', { input: { personaId, conversationId } })
|
||||
.then(setConversation)
|
||||
.catch((error) => setNotice(errorText(error)))
|
||||
}, [personaId, conversationId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!newConversationRequest || newConversationRequest === handledNewConversationRequest.current || !personaId) return
|
||||
handledNewConversationRequest.current = newConversationRequest
|
||||
void createConversation()
|
||||
}, [newConversationRequest, personaId])
|
||||
|
||||
useEffect(() => {
|
||||
ledgerRef.current?.scrollTo({ top: ledgerRef.current.scrollHeight, behavior: 'smooth' })
|
||||
}, [conversation?.messages.length])
|
||||
|
||||
const createConversationEntry = async (event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
if (!personaName.trim()) return
|
||||
setLoading(true)
|
||||
try {
|
||||
const next = await invoke<PersonaBodySnapshot>('register_trial_persona', { input: { displayName: personaName.trim() } })
|
||||
setBody(next)
|
||||
setPersonaId(next.personas.at(-1)?.personaId || '')
|
||||
setNotice('对话入口已建立。当前入口保存在本机,可在设置中继续管理。')
|
||||
} catch (error) { setNotice(errorText(error)) }
|
||||
finally { setLoading(false) }
|
||||
}
|
||||
|
||||
const send = async (event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
if (!draft.trim() || !personaId || !conversationId || !providerId) return
|
||||
const content = draft.trim()
|
||||
const requestId = `HLP_AGENT_${crypto.randomUUID().replaceAll('-', '')}`
|
||||
activeRequestRef.current = requestId
|
||||
setDraft('')
|
||||
setSending(true)
|
||||
setNotice('')
|
||||
setActivity([{
|
||||
requestId,
|
||||
conversationId,
|
||||
stage: 'MESSAGE_PERSISTED',
|
||||
label: '正在把消息写入频道',
|
||||
state: 'RUNNING',
|
||||
completed: 0,
|
||||
total: 1,
|
||||
occurredAtUnixMs: Date.now(),
|
||||
}])
|
||||
setConversation((current) => current ? {
|
||||
...current,
|
||||
messages: [...current.messages, {
|
||||
messageId: `optimistic-${requestId}`,
|
||||
eventId: requestId,
|
||||
personaId,
|
||||
role: 'human',
|
||||
content,
|
||||
providerId,
|
||||
model,
|
||||
createdAtUnixMs: Date.now(),
|
||||
stateVersion: current.stateVersion + 1,
|
||||
receiptHash: '',
|
||||
toolReceipts: [],
|
||||
}],
|
||||
} : current)
|
||||
try {
|
||||
const next = await invoke<ConversationSnapshot>('send_message', { input: {
|
||||
personaId,
|
||||
conversationId,
|
||||
providerId,
|
||||
model: model || null,
|
||||
content,
|
||||
useKnowledge: true,
|
||||
activeKnowledgeSource: knowledgeFocus?.source || null,
|
||||
activeKnowledgePath: knowledgeFocus?.path || null,
|
||||
requestId,
|
||||
} })
|
||||
setConversation(next)
|
||||
await refreshConversations(personaId, next.conversationId)
|
||||
setNotice(next.messages.at(-1)?.toolReceipts.length ? '已完成,并附上本轮实际读取的知识来源。' : '已完成。')
|
||||
} catch (error) {
|
||||
setNotice(errorText(error))
|
||||
void invoke<ConversationSnapshot>('get_conversation', { input: { personaId, conversationId } }).then(setConversation)
|
||||
}
|
||||
finally { setSending(false) }
|
||||
}
|
||||
|
||||
const saveProvider = async (event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
setLoading(true)
|
||||
try {
|
||||
const existingProviderId = selectedProvider?.source === 'EXPLICIT_USER_CONFIG'
|
||||
? selectedProvider.providerId
|
||||
: null
|
||||
const next = await invoke<ProviderDiscovery>('upsert_model_provider', { input: {
|
||||
providerId: existingProviderId,
|
||||
label: configLabel.trim(),
|
||||
kind: 'OPENAI_COMPATIBLE',
|
||||
baseUrl: configBaseUrl.trim(),
|
||||
model: configModel.trim(),
|
||||
apiKey: configKey || null,
|
||||
} })
|
||||
setDiscovery(next)
|
||||
const saved = (existingProviderId && next.providers.find((item) => item.providerId === existingProviderId))
|
||||
|| next.providers.find((item) => item.label === configLabel.trim())
|
||||
selectProvider(saved?.providerId || next.providers.find((item) => item.state === 'AVAILABLE')?.providerId || '')
|
||||
setModel(saved?.selectedModel || configModel.trim())
|
||||
setConfigKey('')
|
||||
setSettingsOpen(false)
|
||||
setNotice('模型入口已保存并验证;密钥只进入系统钥匙串。')
|
||||
} catch (error) { setNotice(errorText(error)) }
|
||||
finally { setLoading(false) }
|
||||
}
|
||||
|
||||
const runLimbDrill = async () => {
|
||||
setLimbBusy(true)
|
||||
setNotice('ZY-LIMB-002 正在只读沙箱内执行边界回归课……')
|
||||
try {
|
||||
const receipt = await invoke<DrillReceipt>('run_execution_limb_training_drill', { input: { lessonId: 'brain-boundary-and-evidence-v1' } })
|
||||
setLimb(receipt.snapshot)
|
||||
setNotice(`${receipt.state} · ${receipt.summary} · ${shortHash(receipt.receiptHash)}`)
|
||||
} catch (error) { setNotice(errorText(error)) }
|
||||
finally { setLimbBusy(false) }
|
||||
}
|
||||
|
||||
const compileHldpPreview = async () => {
|
||||
setForgeBusy(true)
|
||||
setGirPreview(null)
|
||||
try {
|
||||
const program = JSON.parse(hldpProgram) as Record<string, unknown>
|
||||
const result = await invoke<GirPreview>('preview_hldp_tool_program', { input: program })
|
||||
setGirPreview(result)
|
||||
setNotice('HLDP 已编译为确定性 GIR 预览;本步没有执行工具,也没有取得新权限。')
|
||||
} catch (error) {
|
||||
setNotice(errorText(error))
|
||||
} finally {
|
||||
setForgeBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const personaLabel = selectedPersona?.state === 'IMMUTABLE_ACTIVE'
|
||||
? `${selectedPersona.displayName} · ${selectedPersona.personaId}`
|
||||
: `未绑定人格体 · ${selectedPersona?.personaId || '等待登记'}`
|
||||
const modelLabel = selectedProvider
|
||||
? `${selectedProvider.label}${model ? ` · ${model}` : ''}`
|
||||
: '尚未接入模型'
|
||||
|
||||
if (loading && !body) return <section className={`persona-chat-world${embedded ? ' is-embedded' : ''}`}><div className="persona-chat-loading">正在打开语言频道……</div></section>
|
||||
|
||||
return <section className={`persona-chat-world${embedded ? ' is-embedded' : ''}`}>
|
||||
<header className="persona-chat-header">
|
||||
<div>
|
||||
{!embedded && onBack && <button type="button" onClick={onBack}>← 返回光之湖</button>}
|
||||
<span>ZERO CORE · LANGUAGE PERSONA CHANNEL</span>
|
||||
<h1>{conversation?.title || (embedded ? '对话与知识协作' : '语言人格频道')}</h1>
|
||||
<p>{personaLabel} <span className="persona-header-divider">/</span> 冰朔 · ICE-GL∞</p>
|
||||
</div>
|
||||
<div className="persona-chat-status">
|
||||
<i className={providerId ? 'ready' : ''}/>
|
||||
<span title={modelLabel}>{modelLabel}</span>
|
||||
<button type="button" onClick={() => setForgeOpen((value) => !value)}>{forgeOpen ? '收起 HLDP' : 'HLDP 工具'}</button>
|
||||
<button type="button" onClick={() => setSettingsOpen((value) => !value)}>{settingsOpen ? '收起设置' : '设置'}</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{!body?.personas.length ? <form className="persona-chat-onboarding" onSubmit={(event) => void createConversationEntry(event)}>
|
||||
<div><span>建立本机对话入口</span><h2>设置频道显示名称</h2><p>该名称用于当前设备上的频道列表,不会更改模型配置或系统权限。</p></div>
|
||||
<input value={personaName} maxLength={80} aria-label="频道显示名称" onChange={(event) => setPersonaName(event.target.value)}/>
|
||||
<button disabled={loading || !personaName.trim()}>进入对话</button>
|
||||
</form> : <div className="persona-chat-body">
|
||||
<aside className="persona-conversation-rail" aria-label="对话列表">
|
||||
<header><div><span>CONVERSATIONS</span><b>对话</b></div><button type="button" onClick={() => void createConversation()} disabled={loading}>+ 新建</button></header>
|
||||
<div>{conversations.map((item) => <article className={item.conversationId === conversationId ? 'active' : ''} key={item.conversationId}>
|
||||
<button type="button" onClick={() => setConversationId(item.conversationId)}><b>{item.title}</b><span>{item.messageCount ? `${item.messageCount} 条消息` : '空对话'}</span><small>{item.lastMessagePreview || '等待第一句话'}</small></button>
|
||||
<button className="persona-conversation-delete" type="button" title="删除对话" aria-label={`删除对话 ${item.title}`} onClick={() => void deleteConversation(item)}>×</button>
|
||||
</article>)}</div>
|
||||
{!conversations.length && <p>还没有对话。新建后,每条历史会拥有独立坐标。</p>}
|
||||
</aside>
|
||||
<main className="persona-chat-main">
|
||||
<div className="persona-chat-ledger" ref={ledgerRef} aria-live="polite">
|
||||
{conversation?.messages.length ? conversation.messages.map((item) => <article className={`persona-chat-message is-${item.role}`} key={item.messageId}>
|
||||
<header><b>{item.role === 'human' ? '冰朔 · ICE-GL∞' : personaLabel}</b><span>{localTime(item.createdAtUnixMs)}</span></header>
|
||||
{item.role === 'persona'
|
||||
? <div className="persona-message-markdown" dangerouslySetInnerHTML={{ __html: markdownHtml(item.content) }}/>
|
||||
: <p>{item.content}</p>}
|
||||
{item.toolReceipts.length > 0 && <div className="persona-knowledge-receipts">{item.toolReceipts.map((receipt) => <span key={`${item.messageId}-${receipt.path}`} title={receipt.contentSha256}>引用知识 · {receipt.path} · {shortHash(receipt.contentSha256)}</span>)}</div>}
|
||||
{item.role === 'persona' && <footer><span>{selectedProvider?.label || '模型入口'} · {item.model || model || '自动路由'}</span><span title={item.receiptHash}>已写入频道</span></footer>}
|
||||
</article>) : <div className="persona-chat-empty"><span>{conversation ? '频道已就绪' : '尚未选择对话'}</span><h2>{conversation ? (knowledgeFocus ? '询问当前知识页' : '开始新对话') : '先建立一个对话'}</h2><p>{conversation ? (knowledgeFocus ? '回复会附带知识路径与内容指纹,便于核验引用来源。' : '输入“@知识 关键词”可检索本地知识库。') : '每个对话拥有独立历史坐标;删除只从活动列表移除并保留本机审计墓碑。'}</p>{!conversation && <button type="button" onClick={() => void createConversation()}>新建对话</button>}</div>}
|
||||
</div>
|
||||
{(sending || activity.length > 0) && <div className="persona-agent-activity" aria-live="polite">
|
||||
{activity.map((item) => <div className={`is-${item.state.toLowerCase()}`} key={item.stage}>
|
||||
<i/><span>{item.label}</span>{item.detail && <small>{item.detail}</small>}
|
||||
</div>)}
|
||||
</div>}
|
||||
<form className="persona-chat-composer" onSubmit={(event) => void send(event)}>
|
||||
{knowledgeFocus && <div className="persona-knowledge-focus"><span>当前知识</span><b>{knowledgeFocus.title}</b><small>{knowledgeFocus.path}</small></div>}
|
||||
<textarea disabled={!conversationId} autoFocus={!embedded} value={draft} maxLength={64000} placeholder={conversationId ? (knowledgeFocus ? `输入关于《${knowledgeFocus.title}》的问题` : '输入消息;需要知识时可直接提问。') : '请先新建或选择对话'} onChange={(event) => setDraft(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter' && !event.shiftKey && !event.nativeEvent.isComposing) { event.preventDefault(); event.currentTarget.form?.requestSubmit() } }}/>
|
||||
<div><span>Enter 发送 · Shift+Enter 换行 · 每次回复保存事件和回执</span><button disabled={sending || !draft.trim() || !providerId || !conversationId}>{sending ? '正在回应…' : '发送'}</button></div>
|
||||
</form>
|
||||
</main>
|
||||
</div>}
|
||||
|
||||
{forgeOpen && <aside className="persona-hldp-forge">
|
||||
<header><div><span>HLDP NATIVE TOOL FORGE</span><h2>人格体原生工具面板</h2><p>类型化 HLDP-NP → 确定性 GIR。预检不会执行代码、访问终端或取得权限。</p></div><button type="button" onClick={() => setForgeOpen(false)}>关闭</button></header>
|
||||
<div className="persona-hldp-forge-body">
|
||||
<label><span>HLDP-NP 程序</span><textarea spellCheck={false} value={hldpProgram} onChange={(event) => setHldpProgram(event.target.value)}/></label>
|
||||
<section><span>GIR 编译回执</span>{girPreview ? <><b>{girPreview.programId}</b><small>GIR · {shortHash(girPreview.girSha256)}</small><small>源码 · {shortHash(girPreview.sourceSha256)}</small><ol>{girPreview.actionGraph.map((action) => <li key={action.id}>{action.ordinal + 1}. {action.kind} · {action.id} · {action.permission}</li>)}</ol></> : <p>尚未编译。输入必须是严格类型化结构;自然语言不会被偷偷执行。</p>}</section>
|
||||
</div>
|
||||
<footer><span>下一执行门:授权测试 → 沙箱 → Terminal 回执(本面板当前只开放确定性编译预检)</span><button type="button" disabled={forgeBusy} onClick={() => void compileHldpPreview()}>{forgeBusy ? '正在编译…' : '编译 GIR 预览'}</button></footer>
|
||||
</aside>}
|
||||
|
||||
{settingsOpen && <aside className="persona-chat-settings">
|
||||
<header><div><span>ADVANCED SETTINGS</span><h2>模型与执行证据</h2></div><button type="button" onClick={() => void refreshProviders()}>重新发现</button></header>
|
||||
<div className="persona-provider-selectors">
|
||||
<label><span>频道对象</span><select value={personaId} onChange={(event) => setPersonaId(event.target.value)}>{body?.personas.map((item) => <option value={item.personaId} key={item.personaId}>{item.displayName}</option>)}</select></label>
|
||||
<label><span>模型入口</span><select value={providerId} onChange={(event) => selectProvider(event.target.value)}><option value="">请选择</option>{discovery?.providers.map((item) => <option value={item.providerId} key={item.providerId} disabled={item.state !== 'AVAILABLE'}>{item.label} · {item.state}</option>)}</select></label>
|
||||
<label><span>模型</span><input value={model} onChange={(event) => setModel(event.target.value)}/></label>
|
||||
</div>
|
||||
<form className="persona-provider-form" onSubmit={(event) => void saveProvider(event)}>
|
||||
<input aria-label="模型入口名称" value={configLabel} onChange={(event) => setConfigLabel(event.target.value)}/>
|
||||
<input aria-label="模型基础地址" value={configBaseUrl} onChange={(event) => setConfigBaseUrl(event.target.value)}/>
|
||||
<input aria-label="模型名称" placeholder="模型名称" value={configModel} onChange={(event) => setConfigModel(event.target.value)}/>
|
||||
<input aria-label="API 密钥" type="password" autoComplete="off" placeholder="API 密钥 · 只进钥匙串" value={configKey} onChange={(event) => setConfigKey(event.target.value)}/>
|
||||
<button disabled={!configLabel.trim() || !configBaseUrl.trim()}>保存并验证</button>
|
||||
</form>
|
||||
{limb && <section className="persona-limb-strip"><div><span>专属执行手脚</span><b>{limb.limbId} · {limb.executableState}</b><small>{limb.executableVersion || '未发现版本'} · 训练 {limb.trainingCycleCount} · 验证记忆 {limb.validatedMemoryCount} · {limb.ledgerIntegrity}</small></div><button type="button" disabled={limbBusy || limb.executableState !== 'AVAILABLE'} onClick={() => void runLimbDrill()}>{limbBusy ? '正在训练…' : '运行边界回归课'}</button></section>}
|
||||
</aside>}
|
||||
{notice && <p className="persona-chat-notice">{notice}</p>}
|
||||
</section>
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
.persona-chat-world { min-height: 0; height: 100%; display: flex; flex-direction: column; gap: 16px; padding: 24px clamp(22px,4vw,56px) 32px; color: var(--content-primary); overflow: auto; }
|
||||
.persona-chat-world.is-embedded { padding: 0; gap: 10px; overflow: hidden; }
|
||||
.persona-chat-loading { margin: auto; color: var(--content-secondary); }
|
||||
.persona-chat-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; }
|
||||
.persona-chat-header>div:first-child { min-width: 0; }
|
||||
.persona-chat-header button { border: 0; background: transparent; color: var(--content-secondary); cursor: pointer; padding: 5px 0; }
|
||||
.persona-chat-header span { display: block; color: var(--content-muted); font-size: 10px; letter-spacing: .16em; font-weight: 650; }
|
||||
.persona-chat-header h1 { margin: 5px 0 4px; font-size: clamp(26px,3.5vw,42px); letter-spacing: -.035em; }
|
||||
.persona-chat-world.is-embedded .persona-chat-header h1 { font-size: 22px; }
|
||||
.persona-chat-header p { margin: 0; color: var(--content-secondary); line-height: 1.55; }
|
||||
.persona-chat-header p b { color: var(--content-primary); }
|
||||
.persona-chat-status { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; padding: 9px 11px; border-radius: 13px; background: color-mix(in srgb,var(--panel-bg) 76%,transparent); border: 1px solid color-mix(in srgb,var(--content-primary) 11%,transparent); }
|
||||
.persona-chat-status i { width: 7px; height: 7px; border-radius: 50%; background: var(--content-muted); box-shadow: 0 0 0 4px color-mix(in srgb,var(--content-muted) 12%,transparent); }
|
||||
.persona-chat-status i.ready { background: var(--accent-light); box-shadow: 0 0 12px color-mix(in srgb,var(--accent-light) 65%,transparent); }
|
||||
.persona-chat-status span { max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; letter-spacing: 0; }
|
||||
.persona-chat-status button { border-left: 1px solid color-mix(in srgb,var(--content-primary) 12%,transparent); padding-left: 9px; }
|
||||
.persona-chat-onboarding { margin: auto; width: min(720px,100%); display: grid; grid-template-columns: 1fr auto; gap: 13px; padding: 26px; border-radius: 23px; background: color-mix(in srgb,var(--panel-bg) 82%,transparent); border: 1px solid color-mix(in srgb,var(--content-primary) 12%,transparent); box-shadow: 0 24px 70px rgba(0,0,0,.18); }
|
||||
.persona-chat-onboarding>div { grid-column: 1/-1; }.persona-chat-onboarding span { color: var(--accent-light); font-size: 11px; letter-spacing: .12em; }.persona-chat-onboarding h2 { margin: 7px 0; }.persona-chat-onboarding p { margin: 0; color: var(--content-secondary); line-height: 1.6; }
|
||||
.persona-chat-onboarding input,.persona-provider-selectors select,.persona-provider-selectors input,.persona-provider-form input { min-width: 0; border: 1px solid color-mix(in srgb,var(--content-primary) 14%,transparent); border-radius: 12px; padding: 11px 12px; background: color-mix(in srgb,var(--surface-depth) 86%,transparent); color: var(--content-primary); font: inherit; }
|
||||
.persona-chat-onboarding button,.persona-chat-composer button,.persona-provider-form button,.persona-limb-strip button { border: 0; border-radius: 12px; padding: 11px 17px; background: var(--accent-light); color: var(--surface-depth); font-weight: 700; cursor: pointer; }
|
||||
.persona-chat-body { flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: minmax(190px,240px) minmax(0,1fr); gap: 12px; }
|
||||
.persona-conversation-rail { min-height: 0; display: flex; flex-direction: column; gap: 10px; padding: 12px; border-radius: 18px; background: color-mix(in srgb,var(--surface-depth) 62%,transparent); border: 1px solid color-mix(in srgb,var(--content-primary) 9%,transparent); }
|
||||
.persona-conversation-rail>header { display: flex; align-items: center; justify-content: space-between; gap: 8px; }.persona-conversation-rail>header>div { display: grid; gap: 2px; }.persona-conversation-rail>header span { color: var(--content-muted); font-size: 9px; letter-spacing: .12em; }.persona-conversation-rail>header b { font-size: 14px; }.persona-conversation-rail>header button { border: 0; border-radius: 9px; padding: 7px 9px; background: color-mix(in srgb,var(--accent-light) 12%,transparent); color: var(--accent-light); cursor: pointer; }
|
||||
.persona-conversation-rail>div { min-height: 0; overflow: auto; display: grid; align-content: start; gap: 6px; }.persona-conversation-rail article { position: relative; border-radius: 12px; border: 1px solid transparent; background: color-mix(in srgb,var(--panel-bg) 60%,transparent); }.persona-conversation-rail article.active { border-color: color-mix(in srgb,var(--accent-light) 36%,transparent); background: color-mix(in srgb,var(--accent-light) 9%,var(--panel-bg)); }
|
||||
.persona-conversation-rail article>button:first-child { width: 100%; display: grid; gap: 3px; padding: 10px 28px 10px 10px; text-align: left; border: 0; background: transparent; color: var(--content-primary); cursor: pointer; }.persona-conversation-rail article b,.persona-conversation-rail article small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.persona-conversation-rail article span,.persona-conversation-rail article small,.persona-conversation-rail>p { color: var(--content-muted); font-size: 10px; }.persona-conversation-rail>p { line-height: 1.55; }
|
||||
.persona-conversation-delete { position: absolute; top: 7px; right: 7px; width: 22px; height: 22px; border: 0; border-radius: 50%; background: transparent; color: var(--content-muted); cursor: pointer; }.persona-conversation-delete:hover { background: color-mix(in srgb,#f16f75 15%,transparent); color: #f16f75; }
|
||||
.persona-chat-main { min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 10px; }.persona-chat-empty button { margin-top: 14px; border: 0; border-radius: 11px; padding: 9px 13px; background: var(--accent-light); color: var(--surface-depth); font-weight: 700; cursor: pointer; }
|
||||
.persona-chat-ledger { flex: 1 1 auto; min-height: 250px; display: flex; flex-direction: column; gap: 12px; overflow: auto; padding: 20px; border-radius: 20px; background: color-mix(in srgb,var(--surface-depth) 55%,transparent); border: 1px solid color-mix(in srgb,var(--content-primary) 9%,transparent); }
|
||||
.persona-chat-world.is-embedded .persona-chat-ledger { min-height: 190px; padding: 13px; }
|
||||
.persona-chat-empty { margin: auto; text-align: center; max-width: 560px; color: var(--content-secondary); }.persona-chat-empty>span { color: var(--accent-light); font-size: 11px; letter-spacing: .13em; }.persona-chat-empty h2 { color: var(--content-primary); margin: 7px 0; }.persona-chat-empty p { margin: 0; line-height: 1.6; }
|
||||
.persona-chat-message { width: min(82%,840px); padding: 14px 16px; border-radius: 17px 17px 17px 6px; background: color-mix(in srgb,var(--panel-bg) 88%,transparent); border: 1px solid color-mix(in srgb,var(--content-primary) 10%,transparent); box-shadow: 0 10px 28px rgba(2,7,20,.09); }
|
||||
.persona-chat-message.is-human { align-self: flex-end; border-radius: 17px 17px 6px 17px; background: color-mix(in srgb,var(--accent-light) 10%,var(--panel-bg)); }
|
||||
.persona-chat-message header,.persona-chat-message footer { display: flex; justify-content: space-between; gap: 12px; align-items: center; }.persona-chat-message header span,.persona-chat-message footer { color: var(--content-muted); font-size: 10px; }.persona-chat-message p { margin: 10px 0; line-height: 1.65; white-space: pre-wrap; }
|
||||
.persona-chat-message footer { padding-top: 8px; border-top: 1px solid color-mix(in srgb,var(--content-primary) 8%,transparent); }
|
||||
.persona-knowledge-receipts { display: grid; gap: 5px; margin: 10px 0; }.persona-knowledge-receipts span { padding: 7px 9px; border-radius: 9px; background: color-mix(in srgb,var(--accent-light) 9%,transparent); color: var(--content-secondary); font-size: 11px; overflow-wrap: anywhere; }
|
||||
.persona-chat-composer { display: grid; gap: 9px; }.persona-chat-composer textarea { width: 100%; min-height: 96px; resize: vertical; border: 1px solid color-mix(in srgb,var(--content-primary) 15%,transparent); border-radius: 16px; padding: 14px; background: color-mix(in srgb,var(--panel-bg) 86%,transparent); color: var(--content-primary); font: inherit; line-height: 1.55; box-shadow: 0 14px 38px rgba(2,8,22,.12); }
|
||||
.persona-chat-world.is-embedded .persona-chat-composer textarea { min-height: 78px; }
|
||||
.persona-chat-composer>div:last-child { display: flex; align-items: center; gap: 12px; color: var(--content-muted); font-size: 11px; }.persona-chat-composer>div:last-child span { flex: 1; }.persona-chat-composer button:disabled,.persona-limb-strip button:disabled { opacity: .45; cursor: default; }
|
||||
.persona-knowledge-focus { display: grid; grid-template-columns: auto auto 1fr; gap: 7px 10px; align-items: center; padding: 8px 11px; border-radius: 12px; background: color-mix(in srgb,var(--accent-light) 8%,transparent); border: 1px solid color-mix(in srgb,var(--accent-light) 18%,transparent); }.persona-knowledge-focus span { font-size: 10px; letter-spacing: .1em; color: var(--accent-light); }.persona-knowledge-focus small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--content-muted); }
|
||||
.persona-chat-settings { display: grid; gap: 13px; padding: 16px; border-radius: 17px; background: color-mix(in srgb,var(--panel-bg) 90%,transparent); border: 1px solid color-mix(in srgb,var(--content-primary) 11%,transparent); }.persona-chat-settings>header { display: flex; justify-content: space-between; gap: 12px; }.persona-chat-settings h2 { margin: 4px 0 0; }.persona-chat-settings>header span { font-size: 10px; letter-spacing: .14em; color: var(--content-muted); }.persona-chat-settings>header button { border: 0; background: transparent; color: var(--accent-light); cursor: pointer; }
|
||||
.persona-provider-selectors { display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); gap: 9px; }.persona-provider-selectors label { display: grid; gap: 5px; }.persona-provider-selectors label>span { color: var(--content-muted); font-size: 10px; }
|
||||
.persona-provider-form { display: grid; grid-template-columns: .8fr 1.5fr .8fr 1fr auto; gap: 8px; }
|
||||
.persona-limb-strip { display: flex; align-items: center; justify-content: space-between; gap: 18px; padding: 13px; border-radius: 13px; background: color-mix(in srgb,var(--surface-depth) 68%,transparent); }.persona-limb-strip>div { display: grid; gap: 3px; }.persona-limb-strip span,.persona-limb-strip small { color: var(--content-muted); font-size: 10px; }.persona-limb-strip b { font-size: 13px; }
|
||||
.persona-chat-notice { margin: 0; color: var(--accent-light); font-size: 12px; }
|
||||
@media (max-width: 900px) { .persona-chat-header { flex-direction: column; }.persona-chat-status { width: 100%; }.persona-chat-status span { flex: 1; max-width: none; }.persona-chat-body { grid-template-columns: 1fr; }.persona-conversation-rail { min-height: auto; max-height: 180px; }.persona-provider-selectors,.persona-provider-form { grid-template-columns: 1fr; }.persona-chat-message { width: 94%; } }
|
||||
|
||||
/* HLP-AGENT-WORKBENCH-20260820
|
||||
紧凑工作台覆盖层:保留旧主题资产,不再用巨型标题、卡片墙和裸错误承载Agent。 */
|
||||
.persona-chat-world {
|
||||
--agent-line: color-mix(in srgb,var(--content-primary) 10%,transparent);
|
||||
--agent-panel: color-mix(in srgb,var(--panel-bg) 88%,transparent);
|
||||
position: relative;
|
||||
gap: 10px;
|
||||
padding: 14px clamp(14px,2.4vw,28px) 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.persona-chat-world.is-embedded { padding: 0; }
|
||||
.persona-chat-header { min-height: 54px; align-items: center; padding: 0 2px 9px; border-bottom: 1px solid var(--agent-line); }
|
||||
.persona-chat-header span { font-size: 9px; }
|
||||
.persona-chat-header h1,.persona-chat-world.is-embedded .persona-chat-header h1 { margin: 3px 0 1px; font-size: 20px; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.persona-chat-header p { font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.persona-chat-header p .persona-header-divider { display: inline; letter-spacing: 0; opacity: .45; }
|
||||
.persona-chat-status { min-width: 0; flex: 0 1 440px; justify-content: flex-end; padding: 0; border: 0; border-radius: 0; background: transparent; }
|
||||
.persona-chat-status span { max-width: 340px; font-size: 11px; }
|
||||
.persona-chat-status button { padding: 6px 9px; border: 1px solid var(--agent-line); border-radius: 8px; }
|
||||
.persona-chat-body { grid-template-columns: 226px minmax(0,1fr); gap: 10px; }
|
||||
.persona-conversation-rail { gap: 8px; padding: 9px 8px; border: 0; border-right: 1px solid var(--agent-line); border-radius: 0; background: color-mix(in srgb,var(--surface-depth) 44%,transparent); }
|
||||
.persona-conversation-rail>header { padding: 0 4px 4px; }
|
||||
.persona-conversation-rail>header button { padding: 6px 8px; border-radius: 8px; }
|
||||
.persona-conversation-rail>div { gap: 3px; }
|
||||
.persona-conversation-rail article { border: 0; border-left: 2px solid transparent; border-radius: 8px; background: transparent; }
|
||||
.persona-conversation-rail article:hover { background: color-mix(in srgb,var(--content-primary) 4%,transparent); }
|
||||
.persona-conversation-rail article.active { border-color: var(--accent-light); background: color-mix(in srgb,var(--accent-light) 7%,transparent); }
|
||||
.persona-conversation-rail article>button:first-child { padding: 9px 27px 9px 10px; }
|
||||
.persona-conversation-delete { top: 6px; right: 4px; }
|
||||
.persona-chat-main { gap: 0; }
|
||||
.persona-chat-ledger,.persona-chat-world.is-embedded .persona-chat-ledger { min-height: 180px; gap: 18px; padding: 18px max(18px,calc((100% - 920px)/2)); border: 0; border-radius: 0; background: transparent; }
|
||||
.persona-chat-message { width: min(88%,760px); padding: 0 4px; border: 0; border-radius: 0; background: transparent; box-shadow: none; }
|
||||
.persona-chat-message.is-persona { align-self: flex-start; }
|
||||
.persona-chat-message.is-human { align-self: flex-end; width: min(76%,680px); padding: 12px 14px; border: 1px solid color-mix(in srgb,var(--accent-light) 16%,transparent); border-radius: 15px 15px 5px 15px; background: color-mix(in srgb,var(--accent-light) 10%,var(--panel-bg)); }
|
||||
.persona-chat-message header b { font-size: 11px; }
|
||||
.persona-chat-message header span,.persona-chat-message footer { font-size: 9px; }
|
||||
.persona-chat-message p { margin: 7px 0; font-size: 14px; line-height: 1.7; overflow-wrap: anywhere; }
|
||||
.persona-chat-message.is-persona p { font-size: 14.5px; }
|
||||
.persona-message-markdown { margin: 7px 0; font-size: 14.5px; line-height: 1.72; overflow-wrap: anywhere; }
|
||||
.persona-message-markdown :where(p,ul,ol,blockquote,pre) { margin: 0 0 11px; }
|
||||
.persona-message-markdown :where(ul,ol) { padding-left: 22px; }
|
||||
.persona-message-markdown li { margin: 4px 0; }
|
||||
.persona-message-markdown :where(h1,h2,h3,h4) { margin: 16px 0 8px; color: var(--content-primary); font-size: 15.5px; line-height: 1.4; }
|
||||
.persona-message-markdown code:not(pre code) { padding: 2px 5px; border-radius: 5px; background: color-mix(in srgb,var(--content-primary) 7%,transparent); font: .88em/1.5 ui-monospace,SFMono-Regular,Menlo,monospace; }
|
||||
.persona-message-markdown pre { overflow: auto; padding: 12px; border: 1px solid var(--agent-line); border-radius: 9px; background: color-mix(in srgb,var(--panel-bg) 80%,#000); font: 12.5px/1.65 ui-monospace,SFMono-Regular,Menlo,monospace; }
|
||||
.persona-message-markdown a { color: var(--accent-light); text-underline-offset: 3px; }
|
||||
.persona-message-markdown > :last-child { margin-bottom: 0; }
|
||||
.persona-chat-message footer { margin-top: 9px; padding-top: 7px; border-top: 1px solid var(--agent-line); }
|
||||
.persona-knowledge-receipts span { border-left: 2px solid color-mix(in srgb,var(--accent-light) 48%,transparent); border-radius: 0; background: color-mix(in srgb,var(--accent-light) 6%,transparent); }
|
||||
.persona-agent-activity { display: flex; flex-wrap: wrap; gap: 5px 13px; padding: 8px max(18px,calc((100% - 920px)/2)); border-top: 1px solid var(--agent-line); color: var(--content-secondary); }
|
||||
.persona-agent-activity>div { min-width: 0; display: flex; align-items: center; gap: 6px; font-size: 10px; }
|
||||
.persona-agent-activity i { width: 6px; height: 6px; flex: 0 0 auto; border-radius: 50%; background: var(--content-muted); }
|
||||
.persona-agent-activity .is-running i { background: var(--accent-light); box-shadow: 0 0 9px color-mix(in srgb,var(--accent-light) 62%,transparent); animation: persona-agent-pulse 1.15s ease-in-out infinite; }
|
||||
.persona-agent-activity .is-done i { background: #85d9bb; }
|
||||
.persona-agent-activity .is-failed i { background: #f18a8f; }
|
||||
.persona-agent-activity small { max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--content-muted); }
|
||||
@keyframes persona-agent-pulse { 50% { opacity: .35; transform: scale(.78); } }
|
||||
.persona-chat-composer { gap: 7px; padding: 10px max(18px,calc((100% - 920px)/2)) 0; border-top: 1px solid var(--agent-line); background: color-mix(in srgb,var(--surface-depth) 44%,transparent); }
|
||||
.persona-chat-composer textarea,.persona-chat-world.is-embedded .persona-chat-composer textarea { min-height: 72px; max-height: 210px; padding: 12px 13px; border-radius: 12px; background: var(--agent-panel); box-shadow: none; outline: none; }
|
||||
.persona-chat-composer textarea:focus { border-color: color-mix(in srgb,var(--accent-light) 44%,transparent); box-shadow: 0 0 0 3px color-mix(in srgb,var(--accent-light) 7%,transparent); }
|
||||
.persona-chat-composer>div:last-child { font-size: 10px; }
|
||||
.persona-knowledge-focus { padding: 6px 8px; border: 0; border-left: 2px solid var(--accent-light); border-radius: 0; background: color-mix(in srgb,var(--accent-light) 5%,transparent); }
|
||||
.persona-chat-settings { position: absolute; z-index: 12; top: 64px; right: clamp(14px,2.4vw,28px); width: min(760px,calc(100% - 36px)); max-height: calc(100% - 80px); overflow: auto; padding: 16px; border-radius: 13px; background: color-mix(in srgb,var(--panel-bg) 97%,#020713); box-shadow: 0 24px 70px rgba(0,0,0,.42); }
|
||||
.persona-hldp-forge { position:absolute; z-index:13; inset:64px clamp(14px,2.4vw,28px) 18px auto; width:min(880px,calc(100% - 36px)); display:flex; flex-direction:column; gap:12px; padding:16px; border:1px solid var(--agent-line); border-radius:13px; background:color-mix(in srgb,var(--panel-bg) 98%,#020713); box-shadow:0 24px 70px rgba(0,0,0,.46); }
|
||||
.persona-hldp-forge>header,.persona-hldp-forge>footer { display:flex; align-items:flex-start; justify-content:space-between; gap:18px; }
|
||||
.persona-hldp-forge>header span,.persona-hldp-forge label>span,.persona-hldp-forge section>span { color:var(--content-muted); font-size:9px; letter-spacing:.12em; }
|
||||
.persona-hldp-forge h2 { margin:4px 0; font-size:18px; }
|
||||
.persona-hldp-forge p { margin:0; color:var(--content-secondary); font-size:11px; line-height:1.55; }
|
||||
.persona-hldp-forge>header button { border:0; background:transparent; color:var(--content-secondary); cursor:pointer; }
|
||||
.persona-hldp-forge-body { min-height:0; flex:1; display:grid; grid-template-columns:minmax(0,1.25fr) minmax(250px,.75fr); gap:12px; }
|
||||
.persona-hldp-forge label { min-height:0; display:flex; flex-direction:column; gap:6px; }
|
||||
.persona-hldp-forge textarea { min-height:0; flex:1; resize:none; padding:12px; border:1px solid var(--agent-line); border-radius:9px; background:color-mix(in srgb,var(--surface-depth) 84%,transparent); color:var(--content-primary); font:11px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace; outline:none; }
|
||||
.persona-hldp-forge section { min-height:0; overflow:auto; display:flex; flex-direction:column; gap:7px; padding:12px; border-left:1px solid var(--agent-line); }
|
||||
.persona-hldp-forge section b { font-size:13px; }
|
||||
.persona-hldp-forge section small { color:var(--content-secondary); font:10px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace; }
|
||||
.persona-hldp-forge ol { margin:5px 0 0; padding-left:17px; color:var(--content-secondary); font:10px/1.7 ui-monospace,SFMono-Regular,Menlo,monospace; }
|
||||
.persona-hldp-forge>footer { align-items:center; padding-top:11px; border-top:1px solid var(--agent-line); }
|
||||
.persona-hldp-forge>footer span { color:var(--content-muted); font-size:10px; }
|
||||
.persona-hldp-forge>footer button { border:0; border-radius:9px; padding:9px 15px; background:var(--accent-light); color:var(--surface-depth); font-weight:760; cursor:pointer; }
|
||||
.persona-hldp-forge>footer button:disabled { opacity:.45; cursor:default; }
|
||||
.persona-chat-settings h2 { font-size: 18px; }
|
||||
.persona-provider-form { grid-template-columns: .8fr 1.4fr .8fr 1fr auto; }
|
||||
.persona-chat-notice { position: absolute; z-index: 14; left: 50%; bottom: 112px; transform: translateX(-50%); max-width: min(640px,84%); padding: 7px 11px; border: 1px solid var(--agent-line); border-radius: 9px; background: color-mix(in srgb,var(--panel-bg) 96%,transparent); color: var(--content-secondary); box-shadow: 0 10px 30px rgba(0,0,0,.25); }
|
||||
@media (max-width:900px) {
|
||||
.persona-chat-header { flex-direction: row; align-items: flex-start; }
|
||||
.persona-chat-status { width: auto; flex: 0 1 50%; }
|
||||
.persona-chat-status span { max-width: 180px; }
|
||||
.persona-chat-body { grid-template-columns: 176px minmax(0,1fr); }
|
||||
.persona-conversation-rail { max-height: none; }
|
||||
.persona-provider-form { grid-template-columns: 1fr; }
|
||||
}
|
||||
@media (max-width:680px) {
|
||||
.persona-chat-header p,.persona-chat-status span { display: none; }
|
||||
.persona-chat-body { grid-template-columns:1fr; }
|
||||
.persona-conversation-rail { max-height:132px; border-right:0; border-bottom:1px solid var(--agent-line); }
|
||||
.persona-chat-message.is-human { width:90%; }
|
||||
.persona-hldp-forge-body { grid-template-columns:1fr; }
|
||||
.persona-hldp-forge section { border-left:0; border-top:1px solid var(--agent-line); }
|
||||
}
|
||||
Loading…
Reference in a new issue