feat: improve agent responses and workspace UI
Add live agent progress, elapsed timing, verified code navigation and rich Markdown rendering. Preserve NanumSquare styling with missing glyph fallback, fix platform availability filtering and unify workspace scrollbar and visualization controls.
This commit is contained in:
@@ -16,19 +16,30 @@ function compile(source, overrides = {}, globals = {}) {
|
||||
}
|
||||
|
||||
const widgets = compile(fs.readFileSync(require.resolve('../src/renderer/src/components/ChatWidgets.tsx'), 'utf8'), {
|
||||
'../i18n': { t: key => key, useLocaleRender() {} }, './chatAttachments': {}, './ChatTextContextMenu': {}, './ChatImageAttachments': {}
|
||||
'../i18n': { t: key => key, useLocaleRender() {} }, './chatAttachments': {}, './ChatTextContextMenu': {}, './ChatImageAttachments': {}, './ChatCodeBlock': {}
|
||||
})
|
||||
const panelSource = ts.createSourceFile('WorkspaceQaPanel.tsx', fs.readFileSync(require.resolve('../src/renderer/src/features/workspace/WorkspaceQaPanel.tsx'), 'utf8'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
|
||||
const declarations = panelSource.statements.filter(node => ts.isFunctionDeclaration(node) && ['AiMessages', 'AgentMessages', 'TeacherMessages'].includes(node.name?.text))
|
||||
const ProgressiveChatResponse = props => React.createElement('div', { 'data-reveal-start': props.message.revealStartedAt, 'data-collapsed': props.defaultCollapsed }, props.message.content)
|
||||
const ChatImageAttachments = props => React.createElement('div', { 'data-chat-image-attachments': props.variant }, props.images.map(image => React.createElement('img', { key: image.url, src: image.url, alt: image.name })))
|
||||
const { AgentWorkDuration } = compile(fs.readFileSync(require.resolve('../src/renderer/src/components/AgentWorkDuration.tsx'), 'utf8'), {
|
||||
'../i18n': { t: (key, values = {}) => key.replace(/\{\{(\w+)\}\}/g, (_, name) => values[name] ?? ''), useLocaleRender() {} }
|
||||
})
|
||||
const { AgentProgressBubble } = compile(fs.readFileSync(require.resolve('../src/renderer/src/components/AgentProgressBubble.tsx'), 'utf8'), {
|
||||
'../i18n': { t: (key, values = {}) => key.replace(/\{\{(\w+)\}\}/g, (_, name) => values[name] ?? ''), useLocaleRender() {} },
|
||||
react: { ...React, useEffect() {}, useRef: () => ({ current: null }) },
|
||||
'./AgentWorkDuration': { AgentWorkDuration }
|
||||
})
|
||||
const messages = compile(declarations.map(node => `export ${node.getText(panelSource)}`).join('\n'), {}, {
|
||||
t: key => key, useLocaleRender() {},
|
||||
ChatResponseSkeleton: widgets.ChatResponseSkeleton,
|
||||
ChatEmptyState: widgets.ChatEmptyState,
|
||||
ProgressiveChatResponse,
|
||||
ChatImageAttachments,
|
||||
AgentProgressBubble,
|
||||
AgentWorkDuration,
|
||||
MessageCopyButton: () => null,
|
||||
agentMessageCopyText: message => message.content,
|
||||
Bot: () => React.createElement('svg'), WandSparkles: () => React.createElement('svg')
|
||||
})
|
||||
|
||||
@@ -76,6 +87,33 @@ test('AI answer forwards reveal timing and collapse settings to the same present
|
||||
assert.match(html, /전체 답변/)
|
||||
})
|
||||
|
||||
for (const dark of [true, false]) {
|
||||
test(`agent progress accumulates only during the request and is replaced by the final reply (${dark ? 'dark' : 'light'})`, () => {
|
||||
const progressSteps = [
|
||||
{ id: 'analysis-0', kind: 'analysis', status: 'ok', round: 1, requests: 1 },
|
||||
{ id: 'tool-0-0', kind: 'tool', name: 'files.list', moduleName: 'Camera', status: 'ok', count: 3 },
|
||||
{ id: 'tool-1-0', kind: 'tool', name: 'files.read', moduleName: 'Camera', path: 'config/camera.yaml', status: 'missing' },
|
||||
{ id: 'tool-1-1', kind: 'tool', name: 'logs.read', moduleName: 'Camera', status: 'error' },
|
||||
{ id: 'tool-1-2', kind: 'tool', name: 'files.search', moduleName: 'Camera', status: 'partial', count: 2 }
|
||||
]
|
||||
const props = { dark, enabled: true, statusLoading: false, pending: true, activity: '조회 결과 분석 중', progressSteps, messages: [] }
|
||||
const html = renderToStaticMarkup(messages.AgentMessages(props))
|
||||
assert.equal((html.match(/data-agent-progress="true"/g) ?? []).length, 1)
|
||||
assert.match(html, /1차 분석 완료 · 추가 조회 1건/)
|
||||
assert.match(html, /Camera · 파일 목록 · 조회 완료 · 3건/)
|
||||
assert.match(html, /Camera · 파일 읽기 · config\/camera.yaml · 대상 없음/)
|
||||
assert.match(html, /Camera · 로그 조회 · 조회 실패/)
|
||||
assert.match(html, /Camera · 파일 검색 · 일부 결과 · 2건/)
|
||||
assert.match(html, /조회 결과 분석 중/)
|
||||
assert.equal((html.match(/agent-response-fade/g) ?? []).length, 5)
|
||||
const final = renderToStaticMarkup(messages.AgentMessages({ ...props, pending: false, messages: [{ id: 'answer', role: 'assistant', content: '최종 답변입니다.', durationMs: 20_900 }] }))
|
||||
assert.doesNotMatch(final, /data-agent-progress|config\/camera.yaml|진행 상황/)
|
||||
assert.match(final, /최종 답변입니다/)
|
||||
assert.match(final, /20초 동안 작업함/)
|
||||
assert.ok(final.indexOf('20초 동안 작업함') < final.indexOf('최종 답변입니다'))
|
||||
})
|
||||
}
|
||||
|
||||
for (const dark of [false, true]) {
|
||||
for (const [mode, expected] of [
|
||||
['AiMessages', /AI에게 질문할<br\/>내용을 입력하세요/],
|
||||
|
||||
Reference in New Issue
Block a user