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.
@@ -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, useRef, useEffect, useCallback } from 'react';
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
- const range = selection.getRangeAt(0);
40
- const rect = range.getBoundingClientRect();
41
- savedRangeRef.current = range.cloneRange();
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: "新对话"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hydro-ai-helper",
3
- "version": "2.0.7",
3
+ "version": "2.2.0",
4
4
  "description": "HydroOJ AI Learning Assistant - 一个教学优先的 AI 辅助学习插件",
5
5
  "main": "dist/index.js",
6
6
  "author": "Alture",