Skip to content

Commit 4c546e5

Browse files
committed
feat: 重构统计结果渲染方式, AI助手现在可以获取当前统计结果
1 parent 2c267b5 commit 4c546e5

29 files changed

+1397
-2098
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
- [2.5.4 让AI助手操作数据筛选](#254-让ai助手操作数据筛选)
3737
- [2.5.5 询问AI助手数据分析建议](#255-询问ai助手数据分析建议)
3838
- [2.5.6 让AI助手介绍PsychPen](#256-让ai助手介绍psychpen)
39+
- [2.5.7 让AI助手解释统计结果](#257-让ai助手解释统计结果)
3940
- [3 变量视图](#3-变量视图)
4041
- [3.1 变量预览](#31-变量预览)
4142
- [3.2 定义缺失值](#32-定义缺失值)
@@ -252,6 +253,13 @@ PsychPen 支持所有实现了 [function calling](https://platform.openai.com/do
252253
253254
![](readme/data-14.png)
254255

256+
#### 2.5.7 让AI助手解释统计结果
257+
258+
> 以下示例使用 `deepseek-chat` (`deepseek-v3-0324` 版本) 模型
259+
260+
| ![](readme/data-15.png) | ![](readme/data-16.png) |
261+
| :---------------------: | :---------------------: |
262+
255263
## 3 变量视图
256264

257265
变量视图包含了一系列变量处理相关的功能
@@ -484,7 +492,7 @@ PsychPen 支持所有实现了 [function calling](https://platform.openai.com/do
484492

485493
在统计视图中, 你可以在页面左上角选择你要进行的统计检验类型, 进入对应的统计检验页面
486494

487-
> 注: 本应用的所有显著性标注 \* 均表示 P < 0.05, ** 表示 P < 0.01, \*** 表示 P < 0.001
495+
> 注: 本应用的所有显著性标注 \* 均表示 P < 0.05, \*\* 表示 P < 0.01, \*\*\* 表示 P < 0.001
488496
489497
![](readme/stat-1.png)
490498

public/smm.png

18.6 KB
Loading

readme/data-15.png

406 KB
Loading

readme/data-16.png

430 KB
Loading

src/components/AI.tsx

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,10 @@ import { Messages } from './assistant/Messages'
2626
import { funcs } from './assistant/funcs'
2727

2828
const GREETTING =
29-
'你好, 我是 PsychPen 的 AI 助手, 可以帮你讲解 PsychPen 的使用方法、探索你的数据集、导出数据、跳转页面、变量标准化/中心化/离散化、生成新变量、筛选数据等. 请问有什么可以帮你的?'
29+
'你好, 我是 PsychPen 的 AI 助手, 可以帮你讲解 PsychPen 的使用方法、探索你的数据集、导出数据、跳转页面、变量标准化/中心化/离散化、生成新变量、筛选数据、解释你当前的统计结果等. 请问有什么可以帮你的?'
3030
const INSTRUCTION =
3131
'你是在线统计分析和数据可视化软件"PsychPen"中的AI助手. \n\n你将收到用户的提问、当前用户导入到软件中的数据集中的变量的信息、PsychPen的使用和开发文档、可以供你调用的工具 (函数) 信息. \n\n你的任务是按照用户的要求, 对用户进行回复, 或调用工具 (函数). 在调用工具 (函数) 前, 请确保你已经明确知晓了用户的意图, 否则请通过进一步和用户对话来确认细节. \n\n你的回复中如果包含数学公式和符号, 请使用 TeX 语法, 并将行内公式用 `$` 包裹 (类似于 Markdown 的行内代码), 将块级公式用 `$$` 包裹 (类似于 Markdown 的代码块).'
32-
function GET_PROMPT(
33-
vars: Variable[],
34-
page: string,
35-
): string {
32+
function GET_PROMPT(vars: Variable[], page: string, stat: string): string {
3633
const varsInfo = vars.map((col) => {
3734
if (col.type === '称名或等级数据') {
3835
return `| ${col.name} | ${col.type} | ${col.valid} | ${col.missing} | ${col.unique} | - | - | - | - | - | - |`
@@ -50,7 +47,7 @@ function GET_PROMPT(
5047
: '未定义任何子变量'
5148
return `| ${col.name} | ${col.type} | ${col.valid} | ${col.missing} | ${col.unique} | ${col.mean?.toFixed(4) || '-'} | ${col.std?.toFixed(4) || '-'} | ${col.q2?.toFixed(4) || '-'} | ${col.min?.toFixed(4) || '-'} | ${col.max?.toFixed(4) || '-'} | ${subVarInfo} |`
5249
})
53-
const userText = `\n\n# 用户信息\n\n用户当前所处的页面为: ${page}`
50+
const userText = `\n\n# 用户信息\n\n用户当前所处的页面为: ${page}${stat && `, 当前统计结果为: \n\n\`\`\`markdown\n${stat}\n\`\`\``}`
5451
const varsText = `\n\n# 变量信息\n\n| 变量名 | 变量类型 | 有效值数量 | 缺失值数量 | 唯一值数量 | 均值 | 标准差 | 中位数 | 最小值 | 最大值 | 子变量信息 |\n| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n${varsInfo.join('\n')}`
5552
const docsText = `\n\n# 使用文档\n\n\`\`\`markdown\n${readme}\n\`\`\``
5653
return INSTRUCTION + userText + varsText + docsText
@@ -71,8 +68,17 @@ export function AI() {
7168
const data = useData((state) => state.data)
7269
const dataCols = useData((state) => state.dataCols)
7370
const messageApi = useStates((state) => state.messageApi)
71+
const statResult = useStates((state) => state.statResult)
7472
const disabled = useStates((state) => state.disabled)
75-
const nav = useNav()
73+
const activeMainPage = useNav((state) => state.activeMainPage)
74+
const currentPageInfo = useNav((state) => state.currentPageInfo)
75+
const setMainPage = useNav((state) => state.setMainPage)
76+
const setStatisticsViewSubPage = useNav(
77+
(state) => state.setStatisticsViewSubPage,
78+
)
79+
const setVariableViewSubPage = useNav((state) => state.setVariableViewSubPage)
80+
const setPlotsViewSubPage = useNav((state) => state.setPlotsViewSubPage)
81+
const setToolsViewSubPage = useNav((state) => state.setToolsViewSubPage)
7682
const [input, setInput] = useState('')
7783
const [loading, setLoading] = useState(false)
7884
const [showLoading, setShowLoading] = useState(false)
@@ -100,7 +106,14 @@ export function AI() {
100106
setMessages([...old, user])
101107
setInput('')
102108
})
103-
const system = GET_PROMPT(dataCols, nav.currentPageInfo())
109+
const system = GET_PROMPT(
110+
dataCols,
111+
currentPageInfo(),
112+
statResult ||
113+
(activeMainPage === MAIN_PAGES_LABELS.STATISTICS
114+
? '(还未进行统计分析)'
115+
: ''),
116+
)
104117

105118
const stream = await ai.chat.completions.create({
106119
model: model,
@@ -239,7 +252,7 @@ export function AI() {
239252
break
240253
}
241254
case 'nav_to_data_view': {
242-
nav.setMainPage(MAIN_PAGES_LABELS.DATA)
255+
setMainPage(MAIN_PAGES_LABELS.DATA)
243256
newMessages[1].content = '已成功跳转到数据视图'
244257
break
245258
}
@@ -251,8 +264,8 @@ export function AI() {
251264
) {
252265
throw new Error(`未知的子页面 (${page})`)
253266
}
254-
nav.setMainPage(MAIN_PAGES_LABELS.VARIABLE)
255-
nav.setVariableViewSubPage(page)
267+
setMainPage(MAIN_PAGES_LABELS.VARIABLE)
268+
setVariableViewSubPage(page)
256269
newMessages[1].content = `已成功跳转到变量视图的${page}页面`
257270
break
258271
}
@@ -264,8 +277,8 @@ export function AI() {
264277
) {
265278
throw new Error(`未知的子页面 (${page})`)
266279
}
267-
nav.setMainPage(MAIN_PAGES_LABELS.PLOTS)
268-
nav.setPlotsViewSubPage(page)
280+
setMainPage(MAIN_PAGES_LABELS.PLOTS)
281+
setPlotsViewSubPage(page)
269282
newMessages[1].content = `已成功跳转到绘图视图的${page}页面`
270283
break
271284
}
@@ -277,8 +290,8 @@ export function AI() {
277290
) {
278291
throw new Error(`未知的子页面 (${page})`)
279292
}
280-
nav.setMainPage(MAIN_PAGES_LABELS.STATISTICS)
281-
nav.setStatisticsViewSubPage(page)
293+
setMainPage(MAIN_PAGES_LABELS.STATISTICS)
294+
setStatisticsViewSubPage(page)
282295
newMessages[1].content = `已成功跳转到统计视图的${page}页面`
283296
break
284297
}
@@ -290,8 +303,8 @@ export function AI() {
290303
) {
291304
throw new Error(`未知的子页面 (${page})`)
292305
}
293-
nav.setMainPage(MAIN_PAGES_LABELS.TOOLS)
294-
nav.setToolsViewSubPage(page)
306+
setMainPage(MAIN_PAGES_LABELS.TOOLS)
307+
setToolsViewSubPage(page)
295308
newMessages[1].content = `已成功跳转到工具视图的${page}页面`
296309
break
297310
}

src/components/assistant/Messages.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,6 @@ import type {
99
import { useEffect, useRef } from 'react'
1010
import { md5 } from '../../lib/utils'
1111
import { ToolCall } from './ToolCall'
12-
import '../../styles/markdown.css'
13-
import katex from 'marked-katex-extension'
14-
import 'katex/dist/katex.min.css'
15-
16-
marked.use(katex({ throwOnError: false }))
1712

1813
export function Messages({
1914
messages,

src/components/assistant/ToolCall.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ function NavToPageTool({
172172
}) {
173173
return (
174174
<div>
175-
跳转到{' '}
175+
跳转到{' '}
176176
<Tag color='blue' style={{ margin: 0 }}>
177177
{mainPageName}
178178
</Tag>
@@ -182,8 +182,8 @@ function NavToPageTool({
182182
下的{' '}
183183
<Tag color='blue' style={{ margin: 0 }}>
184184
{subPageName}
185-
</Tag>
186-
{' '}页面
185+
</Tag>{' '}
186+
页面
187187
</>
188188
)}
189189
</div>

src/components/statistics/CorrReliability.tsx

Lines changed: 26 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
11
import { CorrRealiability } from '@psych/lib'
22
import { Button, Form, Select } from 'antd'
3-
import { useState } from 'react'
3+
import { useEffect, useState } from 'react'
44
import { flushSync } from 'react-dom'
55
import { useData } from '../../lib/hooks/useData'
66
import { useStates } from '../../lib/hooks/useStates'
7-
import { sleep, uuid } from '../../lib/utils'
7+
import { renderStatResult, sleep } from '../../lib/utils'
88

99
type Option = {
1010
/** 变量名 */
1111
variables: [string, string]
1212
/** 分组变量 */
1313
group?: string
1414
}
15-
type Result = {
16-
m: CorrRealiability
17-
} & Option
1815

1916
export function CorrReliability() {
2017
const dataCols = useData((state) => state.dataCols)
2118
const dataRows = useData((state) => state.dataRows)
2219
const isLargeData = useData((state) => state.isLargeData)
2320
const messageApi = useStates((state) => state.messageApi)
24-
const [result, setResult] = useState<Result | null>(null)
21+
const statResult = useStates((state) => state.statResult)
22+
const setStatResult = useStates((state) => state.setStatResult)
23+
useEffect(() => {
24+
setStatResult('')
25+
}, [setStatResult])
2526
const [disabled, setDisabled] = useState<boolean>(false)
2627
const handleCalculate = async (values: Option) => {
2728
try {
@@ -40,7 +41,19 @@ export function CorrReliability() {
4041
const g = values.group
4142
const group = g ? filteredRows.map((row) => String(row[g])) : undefined
4243
const m = new CorrRealiability(x1, x2, group)
43-
setResult({ m, ...values })
44+
setStatResult(`
45+
## 1 重测信度/复本信度分析
46+
47+
对前后测变量"${values.variables[0]}"和"${values.variables[1]}"进行重测信度/复本信度分析 (即皮尔逊相关系数)${group ? `, 分组变量为"${g}"` : ''}.
48+
49+
结果如表 1 所示.
50+
51+
> 表 1 - 重测信度/复本信度分析结果
52+
53+
| 分组 | 相关系数(r<sub>xx</sub>) | 测定系数(r<sub>xx</sub><sup>2</sup>) |
54+
| :---: | :---: | :---: |
55+
${m.r.map((_, i) => `| ${m.group[i]} | ${m.r[i].toFixed(3)} | ${m.r2[i].toFixed(3)} |`).join('\n')}
56+
`)
4457
messageApi?.destroy()
4558
messageApi?.success(`数据处理完成, 用时 ${Date.now() - timestamp} 毫秒`)
4659
} catch (error) {
@@ -105,42 +118,13 @@ export function CorrReliability() {
105118
</div>
106119

107120
<div className='component-result'>
108-
{result ? (
121+
{statResult ? (
109122
<div className='w-full h-full overflow-auto'>
110-
<p className='text-lg mb-2 text-center w-full'>
111-
重测信度/复本信度分析
112-
</p>
113-
<table className='three-line-table'>
114-
<thead>
115-
<tr>
116-
<td>分组</td>
117-
<td>
118-
相关系数(r<sub>xx</sub>)
119-
</td>
120-
<td>
121-
测定系数(r<sub>xx</sub>
122-
<sup>2</sup>)
123-
</td>
124-
</tr>
125-
</thead>
126-
<tbody>
127-
{result.m.r.map((_, i) => (
128-
<tr key={uuid()}>
129-
<td>{result.m.group[i]}</td>
130-
<td>{result.m.r[i].toFixed(3)}</td>
131-
<td>{result.m.r2[i].toFixed(3)}</td>
132-
</tr>
133-
))}
134-
</tbody>
135-
</table>
136-
<p className='text-xs mt-3 text-center w-full'>
137-
配对变量: {result.variables.join(', ')}
138-
</p>
139-
{result.group && (
140-
<p className='text-xs mt-2 text-center w-full'>
141-
分组变量: {result.group}
142-
</p>
143-
)}
123+
<iframe
124+
srcDoc={renderStatResult(statResult)}
125+
className='w-full h-full'
126+
title='statResult'
127+
/>
144128
</div>
145129
) : (
146130
<div className='w-full h-full flex justify-center items-center'>

0 commit comments

Comments
 (0)