hydro-ai-helper 2.0.7 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/handlers/batchSummaryHandler.js +4 -2
- package/dist/handlers/batchSummaryHandler.js.map +1 -1
- package/dist/handlers/teachingSummaryHandler.js +3 -0
- package/dist/handlers/teachingSummaryHandler.js.map +1 -1
- package/dist/index.js +20 -1
- package/dist/index.js.map +1 -1
- package/dist/models/featureStats.js +58 -0
- package/dist/models/featureStats.js.map +1 -0
- package/dist/models/requestStats.js +14 -1
- package/dist/models/requestStats.js.map +1 -1
- package/dist/services/batchSummaryService.js +9 -1
- package/dist/services/batchSummaryService.js.map +1 -1
- package/dist/services/effectivenessService.js +37 -0
- package/dist/services/effectivenessService.js.map +1 -1
- package/dist/services/errorReporter.js +67 -0
- package/dist/services/errorReporter.js.map +1 -1
- package/dist/services/telemetryService.js +13 -2
- package/dist/services/telemetryService.js.map +1 -1
- package/frontend/student/ChatInput.tsx +118 -116
- package/frontend/student/ChatMessageList.tsx +98 -73
- package/frontend/student/ThinkingBlock.tsx +15 -12
- package/frontend/student/hooks/useTextSelection.ts +7 -21
- package/frontend/student/icons.tsx +81 -0
- package/locales/en.yaml +0 -1
- package/locales/zh.yaml +0 -1
- package/package.json +1 -1
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { i18n } from '@hydrooj/ui-default';
|
|
3
|
-
import { COLORS, ANIMATIONS } from '../utils/styles';
|
|
4
3
|
|
|
5
4
|
interface ThinkingBlockProps {
|
|
6
5
|
isStreaming: boolean;
|
|
7
6
|
}
|
|
8
7
|
|
|
8
|
+
/**
|
|
9
|
+
* 方案 A · 克制蓝 — 深度思考指示
|
|
10
|
+
* 三个蓝点的脉冲动画(需要 dotpulse keyframes,已由 ChatMessageList 注入)。
|
|
11
|
+
*/
|
|
9
12
|
export const ThinkingBlock: React.FC<ThinkingBlockProps> = ({ isStreaming }) => {
|
|
10
13
|
if (!isStreaming) return null;
|
|
11
14
|
|
|
15
|
+
const dot = (delay: string): React.CSSProperties => ({
|
|
16
|
+
width: '5px', height: '5px', borderRadius: '50%', background: '#2563eb',
|
|
17
|
+
animation: `dotpulse 1.2s infinite ${delay}`,
|
|
18
|
+
});
|
|
19
|
+
|
|
12
20
|
return (
|
|
13
|
-
<div style={{
|
|
14
|
-
display: 'flex',
|
|
15
|
-
alignItems: 'center',
|
|
16
|
-
gap: '6px',
|
|
17
|
-
padding: '6px 0',
|
|
18
|
-
fontSize: '12px',
|
|
19
|
-
color: COLORS.primary,
|
|
20
|
-
}}>
|
|
21
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
|
|
22
|
-
<circle cx="12" cy="12" r="10" stroke={COLORS.primary} strokeWidth="2" strokeDasharray="31.4" strokeDashoffset="10" style={{ animation: ANIMATIONS.spin }} />
|
|
23
|
-
</svg>
|
|
21
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '2px 0', fontSize: '12.5px', color: '#64748b' }}>
|
|
24
22
|
<span>{i18n('ai_helper_student_thinking')}</span>
|
|
23
|
+
<span style={{ display: 'inline-flex', gap: '3px' }}>
|
|
24
|
+
<span style={dot('0s')} />
|
|
25
|
+
<span style={dot('0.2s')} />
|
|
26
|
+
<span style={dot('0.4s')} />
|
|
27
|
+
</span>
|
|
25
28
|
</div>
|
|
26
29
|
);
|
|
27
30
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState,
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
2
|
|
|
3
3
|
interface UseTextSelectionOptions {
|
|
4
4
|
onClarify: (text: string, sourceId: string) => void;
|
|
@@ -8,20 +8,17 @@ export function useTextSelection({ onClarify }: UseTextSelectionOptions) {
|
|
|
8
8
|
const [selectedText, setSelectedText] = useState('');
|
|
9
9
|
const [selectedSourceAiMessageId, setSelectedSourceAiMessageId] = useState('');
|
|
10
10
|
const [popupPosition, setPopupPosition] = useState<{ x: number; y: number } | null>(null);
|
|
11
|
-
const savedRangeRef = useRef<Range | null>(null);
|
|
12
11
|
const [pendingAutoSubmit, setPendingAutoSubmit] = useState(false);
|
|
13
12
|
|
|
14
13
|
const handleTextSelection = useCallback(() => {
|
|
15
14
|
const selection = window.getSelection();
|
|
16
15
|
if (!selection || selection.isCollapsed) {
|
|
17
16
|
setPopupPosition(null);
|
|
18
|
-
savedRangeRef.current = null;
|
|
19
17
|
return;
|
|
20
18
|
}
|
|
21
19
|
const text = selection.toString().trim();
|
|
22
20
|
if (!text) {
|
|
23
21
|
setPopupPosition(null);
|
|
24
|
-
savedRangeRef.current = null;
|
|
25
22
|
return;
|
|
26
23
|
}
|
|
27
24
|
let node = selection.anchorNode;
|
|
@@ -36,42 +33,31 @@ export function useTextSelection({ onClarify }: UseTextSelectionOptions) {
|
|
|
36
33
|
node = node.parentNode;
|
|
37
34
|
}
|
|
38
35
|
if (isInAiMessage) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
// Only read the selection's geometry to position the popup. We must NOT
|
|
37
|
+
// re-apply the selection (removeAllRanges()/addRange()) afterwards: the
|
|
38
|
+
// native highlight already survives the React re-render, and forcing a
|
|
39
|
+
// programmatic re-selection makes Chrome drop the painted highlight,
|
|
40
|
+
// so the user loses sight of what they selected while the popup is open.
|
|
41
|
+
const rect = selection.getRangeAt(0).getBoundingClientRect();
|
|
42
42
|
setSelectedText(text);
|
|
43
43
|
setSelectedSourceAiMessageId(aiMessageId);
|
|
44
44
|
setPopupPosition({ x: rect.left + rect.width / 2, y: rect.top - 40 });
|
|
45
45
|
} else {
|
|
46
46
|
setPopupPosition(null);
|
|
47
|
-
savedRangeRef.current = null;
|
|
48
47
|
}
|
|
49
48
|
}, []);
|
|
50
49
|
|
|
51
50
|
const handleDontUnderstand = useCallback(() => {
|
|
52
51
|
if (!selectedSourceAiMessageId) {
|
|
53
52
|
setPopupPosition(null);
|
|
54
|
-
savedRangeRef.current = null;
|
|
55
53
|
return;
|
|
56
54
|
}
|
|
57
55
|
const truncated = selectedText.length > 100 ? selectedText.substring(0, 100) + '...' : selectedText;
|
|
58
56
|
onClarify(truncated, selectedSourceAiMessageId);
|
|
59
57
|
setPopupPosition(null);
|
|
60
|
-
savedRangeRef.current = null;
|
|
61
58
|
setPendingAutoSubmit(true);
|
|
62
59
|
}, [selectedText, selectedSourceAiMessageId, onClarify]);
|
|
63
60
|
|
|
64
|
-
// Restore selection after React render
|
|
65
|
-
useEffect(() => {
|
|
66
|
-
if (popupPosition && savedRangeRef.current) {
|
|
67
|
-
const selection = window.getSelection();
|
|
68
|
-
if (selection) {
|
|
69
|
-
selection.removeAllRanges();
|
|
70
|
-
selection.addRange(savedRangeRef.current);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}, [popupPosition]);
|
|
74
|
-
|
|
75
61
|
return {
|
|
76
62
|
selectedText,
|
|
77
63
|
selectedSourceAiMessageId,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 方案 A · 克制蓝 — 统一图标集(单线、随 color 着色)
|
|
5
|
+
* 用于问题类型卡片、输入框操作与发送按钮,替换原先的 emoji。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
type IconProps = { size?: number };
|
|
9
|
+
|
|
10
|
+
const base = (size: number): React.SVGProps<SVGSVGElement> => ({
|
|
11
|
+
width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
|
|
12
|
+
stroke: 'currentColor', strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/** 问题类型图标:understand / think / debug / optimize */
|
|
16
|
+
export const TypeIcon: React.FC<IconProps & { type: string }> = ({ type, size = 14 }) => {
|
|
17
|
+
switch (type) {
|
|
18
|
+
case 'understand':
|
|
19
|
+
return (
|
|
20
|
+
<svg {...base(size)} strokeWidth={2}>
|
|
21
|
+
<circle cx="12" cy="12" r="9" />
|
|
22
|
+
<path d="M9.6 9.3a2.4 2.4 0 0 1 4.3 1.4c0 1.6-2 1.7-2 3.1" />
|
|
23
|
+
<circle cx="11.9" cy="17.2" r="0.9" fill="currentColor" stroke="none" />
|
|
24
|
+
</svg>
|
|
25
|
+
);
|
|
26
|
+
case 'think':
|
|
27
|
+
return (
|
|
28
|
+
<svg {...base(size)} strokeWidth={2}>
|
|
29
|
+
<path d="M9 18h6M10 21h4" />
|
|
30
|
+
<path d="M12 3a6 6 0 0 0-3.8 10.6c.6.5.9 1.1.9 2.4h5.8c0-1.3.3-1.9.9-2.4A6 6 0 0 0 12 3z" />
|
|
31
|
+
</svg>
|
|
32
|
+
);
|
|
33
|
+
case 'debug':
|
|
34
|
+
return (
|
|
35
|
+
<svg {...base(size)} strokeWidth={2}>
|
|
36
|
+
<polyline points="9 8 5 12 9 16" />
|
|
37
|
+
<polyline points="15 8 19 12 15 16" />
|
|
38
|
+
</svg>
|
|
39
|
+
);
|
|
40
|
+
case 'optimize':
|
|
41
|
+
return (
|
|
42
|
+
<svg {...base(size)} strokeWidth={1.8}>
|
|
43
|
+
<path d="M12 3l1.7 6.3L20 11l-6.3 1.7L12 19l-1.7-6.3L4 11l6.3-1.7z" />
|
|
44
|
+
</svg>
|
|
45
|
+
);
|
|
46
|
+
default:
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const SendIcon: React.FC<IconProps> = ({ size = 15 }) => (
|
|
52
|
+
<svg {...base(size)} strokeWidth={2.2}><path d="M5 12h13M13 6l6 6-6 6" /></svg>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
export const AttachIcon: React.FC<IconProps> = ({ size = 12 }) => (
|
|
56
|
+
<svg {...base(size)} strokeWidth={2}>
|
|
57
|
+
<path d="M21 15V8a2 2 0 0 0-2-2h-7l-2-2H5a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h7" />
|
|
58
|
+
</svg>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
export const RefreshIcon: React.FC<IconProps> = ({ size = 12 }) => (
|
|
62
|
+
<svg {...base(size)} strokeWidth={2}><path d="M3 12a9 9 0 1 0 3-6.7L3 8M3 4v4h4" /></svg>
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
export const RemoveIcon: React.FC<IconProps> = ({ size = 11 }) => (
|
|
66
|
+
<svg {...base(size)} strokeWidth={2}><path d="M6 6l12 12M18 6L6 18" /></svg>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
/** 字母标识:替换原机器人 emoji 头像 */
|
|
70
|
+
export const AIMark: React.FC<{ size?: number; radius?: number; fontSize?: number }> = ({
|
|
71
|
+
size = 28, radius = 8, fontSize = 11,
|
|
72
|
+
}) => (
|
|
73
|
+
<div style={{
|
|
74
|
+
width: size, height: size, borderRadius: radius,
|
|
75
|
+
background: 'linear-gradient(135deg, #2563eb, #5b8def)',
|
|
76
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
77
|
+
fontFamily: "'JetBrains Mono', ui-monospace, 'SFMono-Regular', monospace",
|
|
78
|
+
fontSize, fontWeight: 700, color: '#ffffff', letterSpacing: '-0.5px',
|
|
79
|
+
flexShrink: 0,
|
|
80
|
+
}}>AI</div>
|
|
81
|
+
);
|
package/locales/en.yaml
CHANGED
|
@@ -552,7 +552,6 @@ ai_helper_student_optimize_code_required: "Code is required for optimization"
|
|
|
552
552
|
ai_helper_student_cancel: "Cancel"
|
|
553
553
|
ai_helper_student_send_shortcut: "Send (Ctrl+Enter)"
|
|
554
554
|
ai_helper_student_loading: "AI is thinking..."
|
|
555
|
-
ai_helper_student_click_cancel: "Click cancel to stop"
|
|
556
555
|
ai_helper_student_thinking: "Deep thinking..."
|
|
557
556
|
ai_helper_student_retry: "Retry"
|
|
558
557
|
ai_helper_student_new_conversation: "New conversation"
|
package/locales/zh.yaml
CHANGED
|
@@ -552,7 +552,6 @@ ai_helper_student_optimize_code_required: "代码优化需要附带代码"
|
|
|
552
552
|
ai_helper_student_cancel: "取消"
|
|
553
553
|
ai_helper_student_send_shortcut: "发送 (Ctrl+Enter)"
|
|
554
554
|
ai_helper_student_loading: "AI 正在思考..."
|
|
555
|
-
ai_helper_student_click_cancel: "点击取消可中断"
|
|
556
555
|
ai_helper_student_thinking: "正在深度思考..."
|
|
557
556
|
ai_helper_student_retry: "重试"
|
|
558
557
|
ai_helper_student_new_conversation: "新对话"
|