hydro-ai-helper 2.0.3 → 2.0.5
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/README.md +1 -1
- package/frontend/batchSummary/BatchSummaryPanel.tsx +2 -2
- package/frontend/batchSummary/StudentSummaryView.tsx +4 -1
- package/frontend/teachingSummary/TeachingSummaryPanel.tsx +77 -83
- package/frontend/teachingSummary/useTeachingSummary.ts +26 -24
- package/frontend/utils/styles.ts +2 -4
- package/locales/en.yaml +1 -1
- package/locales/zh.yaml +1 -1
- package/package.json +2 -4
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import React, { useEffect, useState, useCallback, useRef, useMemo } from 'react';
|
|
8
8
|
import { i18n } from '@hydrooj/ui-default';
|
|
9
|
-
import { COLORS, SPACING, RADIUS, SHADOWS, getButtonStyle, getAlertStyle, markdownTheme } from '../utils/styles';
|
|
9
|
+
import { COLORS, SPACING, RADIUS, SHADOWS, LAYOUT, getButtonStyle, getAlertStyle, markdownTheme } from '../utils/styles';
|
|
10
10
|
|
|
11
11
|
/** i18n with hardcoded Chinese fallback for keys that may not yet be in lang-*.js */
|
|
12
12
|
const I18N_FALLBACK: Record<string, string> = {
|
|
@@ -287,7 +287,7 @@ export const BatchSummaryPanel: React.FC<BatchSummaryPanelProps> = ({
|
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
return (
|
|
290
|
-
<div style={{ fontFamily: 'inherit', color: COLORS.textPrimary }}>
|
|
290
|
+
<div style={{ fontFamily: 'inherit', color: COLORS.textPrimary, maxWidth: LAYOUT.contentMaxWidth, margin: '0 auto', width: '100%' }}>
|
|
291
291
|
<style>{markdownTheme}</style>
|
|
292
292
|
|
|
293
293
|
{isTeacher && (
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
|
8
8
|
import { i18n } from '@hydrooj/ui-default';
|
|
9
|
-
import { COLORS, SPACING, RADIUS, SHADOWS, markdownTheme } from '../utils/styles';
|
|
9
|
+
import { COLORS, SPACING, RADIUS, SHADOWS, LAYOUT, markdownTheme } from '../utils/styles';
|
|
10
10
|
import { renderMarkdown } from '../utils/markdown';
|
|
11
11
|
|
|
12
12
|
function t(key: string): string {
|
|
@@ -99,6 +99,9 @@ export const StudentSummaryView: React.FC<StudentSummaryViewProps> = ({ domainId
|
|
|
99
99
|
|
|
100
100
|
return (
|
|
101
101
|
<div style={{
|
|
102
|
+
maxWidth: LAYOUT.contentMaxWidth,
|
|
103
|
+
margin: '0 auto',
|
|
104
|
+
width: '100%',
|
|
102
105
|
backgroundColor: COLORS.bgCard,
|
|
103
106
|
borderRadius: RADIUS.md,
|
|
104
107
|
boxShadow: SHADOWS.sm,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
7
7
|
import { i18n } from '@hydrooj/ui-default';
|
|
8
8
|
import {
|
|
9
|
-
COLORS, SPACING, RADIUS, SHADOWS,
|
|
9
|
+
COLORS, SPACING, RADIUS, SHADOWS, TRANSITIONS,
|
|
10
10
|
getButtonStyle, cardStyle, markdownTheme, LAYOUT,
|
|
11
11
|
} from '../utils/styles';
|
|
12
12
|
import { renderMarkdown } from '../utils/markdown';
|
|
@@ -326,16 +326,7 @@ export const TeachingSummaryPanel: React.FC<TeachingSummaryPanelProps> = ({ doma
|
|
|
326
326
|
|
|
327
327
|
const [teachingFocus, setTeachingFocus] = useState('');
|
|
328
328
|
const [feedbackSubmitted, setFeedbackSubmitted] = useState(false);
|
|
329
|
-
|
|
330
|
-
const [isWide, setIsWide] = useState(
|
|
331
|
-
typeof window !== 'undefined' ? window.innerWidth >= LAYOUT.splitBreakpoint : true,
|
|
332
|
-
);
|
|
333
|
-
|
|
334
|
-
useEffect(() => {
|
|
335
|
-
const handleResize = () => setIsWide(window.innerWidth >= LAYOUT.splitBreakpoint);
|
|
336
|
-
window.addEventListener('resize', handleResize);
|
|
337
|
-
return () => window.removeEventListener('resize', handleResize);
|
|
338
|
-
}, []);
|
|
329
|
+
const [suggestionCollapsed, setSuggestionCollapsed] = useState(false);
|
|
339
330
|
|
|
340
331
|
useEffect(() => {
|
|
341
332
|
fetchSummary();
|
|
@@ -486,7 +477,7 @@ export const TeachingSummaryPanel: React.FC<TeachingSummaryPanelProps> = ({ doma
|
|
|
486
477
|
: '';
|
|
487
478
|
|
|
488
479
|
return (
|
|
489
|
-
<div style={{ fontFamily: 'inherit', color: COLORS.textPrimary }}>
|
|
480
|
+
<div style={{ fontFamily: 'inherit', color: COLORS.textPrimary, maxWidth: LAYOUT.contentMaxWidth, margin: '0 auto', width: '100%' }}>
|
|
490
481
|
<style>{markdownTheme}</style>
|
|
491
482
|
|
|
492
483
|
{/* Panel header */}
|
|
@@ -548,19 +539,10 @@ export const TeachingSummaryPanel: React.FC<TeachingSummaryPanelProps> = ({ doma
|
|
|
548
539
|
</div>
|
|
549
540
|
)}
|
|
550
541
|
|
|
551
|
-
{/*
|
|
552
|
-
<
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
flexDirection: isWide ? 'row' : 'column',
|
|
556
|
-
gap: LAYOUT.gap,
|
|
557
|
-
alignItems: 'flex-start',
|
|
558
|
-
}}>
|
|
559
|
-
{/* Main column (60%) — Findings */}
|
|
560
|
-
<div style={{
|
|
561
|
-
flex: isWide ? `0 0 ${LAYOUT.mainColumnWidth}` : '1 1 100%',
|
|
562
|
-
minWidth: 0,
|
|
563
|
-
}}>
|
|
542
|
+
{/* Single-column layout: Findings → AI Suggestion */}
|
|
543
|
+
<div>
|
|
544
|
+
{/* Findings */}
|
|
545
|
+
<div>
|
|
564
546
|
<div style={{
|
|
565
547
|
fontWeight: 600, fontSize: '13px', marginBottom: SPACING.md,
|
|
566
548
|
color: COLORS.textMuted, textTransform: 'uppercase' as const,
|
|
@@ -588,16 +570,9 @@ export const TeachingSummaryPanel: React.FC<TeachingSummaryPanelProps> = ({ doma
|
|
|
588
570
|
)}
|
|
589
571
|
</div>
|
|
590
572
|
|
|
591
|
-
{/*
|
|
573
|
+
{/* AI Suggestion — collapsible conclusion card */}
|
|
592
574
|
{(summary.overallSuggestion || summary.status === 'generating') && (
|
|
593
|
-
<div style={{
|
|
594
|
-
flex: isWide ? `0 0 ${LAYOUT.sidebarWidth}` : '1 1 100%',
|
|
595
|
-
minWidth: 0,
|
|
596
|
-
position: isWide ? 'sticky' as const : 'static' as const,
|
|
597
|
-
top: isWide ? LAYOUT.sidebarStickyTop : undefined,
|
|
598
|
-
maxHeight: isWide ? LAYOUT.sidebarMaxHeight : undefined,
|
|
599
|
-
overflowY: isWide ? 'auto' as const : undefined,
|
|
600
|
-
}}>
|
|
575
|
+
<div style={{ marginTop: LAYOUT.sectionGap }}>
|
|
601
576
|
<div style={{
|
|
602
577
|
border: `1px solid ${COLORS.border}`,
|
|
603
578
|
borderLeft: `4px solid ${COLORS.hydroGreen}`,
|
|
@@ -605,64 +580,83 @@ export const TeachingSummaryPanel: React.FC<TeachingSummaryPanelProps> = ({ doma
|
|
|
605
580
|
backgroundColor: COLORS.bgCard,
|
|
606
581
|
boxShadow: SHADOWS.sm,
|
|
607
582
|
}}>
|
|
608
|
-
{/*
|
|
609
|
-
<div
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
583
|
+
{/* Clickable header */}
|
|
584
|
+
<div
|
|
585
|
+
onClick={() => setSuggestionCollapsed(!suggestionCollapsed)}
|
|
586
|
+
style={{
|
|
587
|
+
padding: `${SPACING.md} ${SPACING.base}`,
|
|
588
|
+
borderBottom: suggestionCollapsed ? 'none' : `1px solid ${COLORS.border}`,
|
|
589
|
+
backgroundColor: COLORS.hydroGreenLight,
|
|
590
|
+
cursor: 'pointer',
|
|
591
|
+
display: 'flex',
|
|
592
|
+
alignItems: 'center',
|
|
593
|
+
justifyContent: 'space-between',
|
|
594
|
+
userSelect: 'none' as const,
|
|
595
|
+
}}
|
|
596
|
+
>
|
|
614
597
|
<span style={{
|
|
615
598
|
fontWeight: 600, fontSize: '13px', color: COLORS.hydroGreenDark,
|
|
616
599
|
textTransform: 'uppercase' as const, letterSpacing: '0.05em',
|
|
617
600
|
}}>
|
|
618
601
|
{t('ai_helper_teaching_summary_overall_suggestion')}
|
|
619
602
|
</span>
|
|
603
|
+
<span style={{
|
|
604
|
+
display: 'inline-block', width: '16px', height: '16px',
|
|
605
|
+
textAlign: 'center' as const, lineHeight: '16px', fontSize: '12px',
|
|
606
|
+
color: COLORS.textMuted,
|
|
607
|
+
transition: `transform ${TRANSITIONS.fast}`,
|
|
608
|
+
transform: suggestionCollapsed ? 'rotate(0deg)' : 'rotate(90deg)',
|
|
609
|
+
}}>▶</span>
|
|
620
610
|
</div>
|
|
621
611
|
|
|
622
|
-
{/*
|
|
623
|
-
{
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
612
|
+
{/* Collapsible content */}
|
|
613
|
+
{!suggestionCollapsed && (
|
|
614
|
+
<>
|
|
615
|
+
{summary.overallSuggestion ? (
|
|
616
|
+
<div
|
|
617
|
+
className="markdown-body"
|
|
618
|
+
style={{ padding: `${SPACING.base} ${SPACING.lg}` }}
|
|
619
|
+
dangerouslySetInnerHTML={{ __html: renderMarkdown(summary.overallSuggestion) }}
|
|
620
|
+
/>
|
|
621
|
+
) : (
|
|
622
|
+
<SkeletonBlock lines={10} />
|
|
623
|
+
)}
|
|
624
|
+
|
|
625
|
+
{/* Feedback */}
|
|
626
|
+
<div style={{
|
|
627
|
+
display: 'flex', alignItems: 'center', justifyContent: 'flex-end',
|
|
628
|
+
gap: SPACING.sm, padding: `${SPACING.sm} ${SPACING.base}`,
|
|
629
|
+
borderTop: `1px solid ${COLORS.border}`,
|
|
630
|
+
}}>
|
|
631
|
+
{feedbackSubmitted ? (
|
|
632
|
+
<span style={{ fontSize: '12px', color: COLORS.successText }}>
|
|
633
|
+
{t('ai_helper_teaching_summary_feedback_thanks')}
|
|
634
|
+
</span>
|
|
635
|
+
) : (
|
|
636
|
+
<>
|
|
637
|
+
{(['up', 'down'] as const).map(rating => (
|
|
638
|
+
<button
|
|
639
|
+
key={rating}
|
|
640
|
+
onClick={() => handleFeedback(rating)}
|
|
641
|
+
style={{
|
|
642
|
+
fontSize: '12px', padding: '3px 8px',
|
|
643
|
+
border: `1px solid ${COLORS.border}`,
|
|
644
|
+
borderRadius: RADIUS.sm,
|
|
645
|
+
backgroundColor: 'transparent',
|
|
646
|
+
color: summary.feedback?.rating === rating
|
|
647
|
+
? (rating === 'up' ? COLORS.successText : COLORS.errorText)
|
|
648
|
+
: COLORS.textMuted,
|
|
649
|
+
cursor: 'pointer',
|
|
650
|
+
}}
|
|
651
|
+
>
|
|
652
|
+
{rating === 'up' ? t('ai_helper_teaching_summary_feedback_helpful') : t('ai_helper_teaching_summary_feedback_not_helpful')}
|
|
653
|
+
</button>
|
|
654
|
+
))}
|
|
655
|
+
</>
|
|
656
|
+
)}
|
|
657
|
+
</div>
|
|
658
|
+
</>
|
|
631
659
|
)}
|
|
632
|
-
|
|
633
|
-
{/* Feedback — inside sidebar */}
|
|
634
|
-
<div style={{
|
|
635
|
-
display: 'flex', alignItems: 'center', justifyContent: 'flex-end',
|
|
636
|
-
gap: SPACING.sm, padding: `${SPACING.sm} ${SPACING.base}`,
|
|
637
|
-
borderTop: `1px solid ${COLORS.border}`,
|
|
638
|
-
}}>
|
|
639
|
-
{feedbackSubmitted ? (
|
|
640
|
-
<span style={{ fontSize: '12px', color: COLORS.successText }}>
|
|
641
|
-
{t('ai_helper_teaching_summary_feedback_thanks')}
|
|
642
|
-
</span>
|
|
643
|
-
) : (
|
|
644
|
-
<>
|
|
645
|
-
{(['up', 'down'] as const).map(rating => (
|
|
646
|
-
<button
|
|
647
|
-
key={rating}
|
|
648
|
-
onClick={() => handleFeedback(rating)}
|
|
649
|
-
style={{
|
|
650
|
-
fontSize: '12px', padding: '3px 8px',
|
|
651
|
-
border: `1px solid ${COLORS.border}`,
|
|
652
|
-
borderRadius: RADIUS.sm,
|
|
653
|
-
backgroundColor: 'transparent',
|
|
654
|
-
color: summary.feedback?.rating === rating
|
|
655
|
-
? (rating === 'up' ? COLORS.successText : COLORS.errorText)
|
|
656
|
-
: COLORS.textMuted,
|
|
657
|
-
cursor: 'pointer',
|
|
658
|
-
}}
|
|
659
|
-
>
|
|
660
|
-
{rating === 'up' ? t('ai_helper_teaching_summary_feedback_helpful') : t('ai_helper_teaching_summary_feedback_not_helpful')}
|
|
661
|
-
</button>
|
|
662
|
-
))}
|
|
663
|
-
</>
|
|
664
|
-
)}
|
|
665
|
-
</div>
|
|
666
660
|
</div>
|
|
667
661
|
</div>
|
|
668
662
|
)}
|
|
@@ -97,6 +97,32 @@ export function useTeachingSummary(domainId: string, contestId: string): UseTeac
|
|
|
97
97
|
};
|
|
98
98
|
}, [stopPolling]);
|
|
99
99
|
|
|
100
|
+
// startPolling must be declared before fetchSummary to avoid TDZ
|
|
101
|
+
// (fetchSummary's dependency array evaluates startPolling immediately)
|
|
102
|
+
const startPolling = useCallback(() => {
|
|
103
|
+
stopPolling();
|
|
104
|
+
pollTimerRef.current = setInterval(async () => {
|
|
105
|
+
try {
|
|
106
|
+
const url = buildUrl(domainId, `/${contestId}`);
|
|
107
|
+
const res = await fetch(url, {
|
|
108
|
+
credentials: 'include',
|
|
109
|
+
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
|
110
|
+
});
|
|
111
|
+
if (!res.ok) return;
|
|
112
|
+
const data = await res.json();
|
|
113
|
+
const fetched: TeachingSummary | null = data.summary ?? null;
|
|
114
|
+
if (fetched) {
|
|
115
|
+
setSummary(fetched);
|
|
116
|
+
if (fetched.status === 'completed' || fetched.status === 'failed') {
|
|
117
|
+
stopPolling();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// ignore transient poll errors
|
|
122
|
+
}
|
|
123
|
+
}, POLL_INTERVAL_MS);
|
|
124
|
+
}, [domainId, contestId, stopPolling]);
|
|
125
|
+
|
|
100
126
|
const fetchSummary = useCallback(async () => {
|
|
101
127
|
setLoading(true);
|
|
102
128
|
setError(null);
|
|
@@ -133,30 +159,6 @@ export function useTeachingSummary(domainId: string, contestId: string): UseTeac
|
|
|
133
159
|
}
|
|
134
160
|
}, [domainId, contestId, startPolling]);
|
|
135
161
|
|
|
136
|
-
const startPolling = useCallback(() => {
|
|
137
|
-
stopPolling();
|
|
138
|
-
pollTimerRef.current = setInterval(async () => {
|
|
139
|
-
try {
|
|
140
|
-
const url = buildUrl(domainId, `/${contestId}`);
|
|
141
|
-
const res = await fetch(url, {
|
|
142
|
-
credentials: 'include',
|
|
143
|
-
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
|
144
|
-
});
|
|
145
|
-
if (!res.ok) return;
|
|
146
|
-
const data = await res.json();
|
|
147
|
-
const fetched: TeachingSummary | null = data.summary ?? null;
|
|
148
|
-
if (fetched) {
|
|
149
|
-
setSummary(fetched);
|
|
150
|
-
if (fetched.status === 'completed' || fetched.status === 'failed') {
|
|
151
|
-
stopPolling();
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
} catch {
|
|
155
|
-
// ignore transient poll errors
|
|
156
|
-
}
|
|
157
|
-
}, POLL_INTERVAL_MS);
|
|
158
|
-
}, [domainId, contestId, stopPolling]);
|
|
159
|
-
|
|
160
162
|
const generate = useCallback(async (teachingFocus?: string, regenerate?: boolean) => {
|
|
161
163
|
setLoading(true);
|
|
162
164
|
setError(null);
|
package/frontend/utils/styles.ts
CHANGED
|
@@ -320,11 +320,9 @@ export const linkStyle: React.CSSProperties = {
|
|
|
320
320
|
|
|
321
321
|
export const LAYOUT = {
|
|
322
322
|
splitBreakpoint: 768,
|
|
323
|
-
|
|
324
|
-
sidebarWidth: '40%',
|
|
325
|
-
sidebarStickyTop: '16px',
|
|
326
|
-
sidebarMaxHeight: 'calc(100vh - 32px)',
|
|
323
|
+
contentMaxWidth: '1080px',
|
|
327
324
|
gap: '24px',
|
|
325
|
+
sectionGap: '32px',
|
|
328
326
|
};
|
|
329
327
|
|
|
330
328
|
// ─── Shared Markdown Theme ───────────────────────────────────────────────────
|
package/locales/en.yaml
CHANGED
|
@@ -436,7 +436,7 @@ ai_helper_admin_version_updating: "Updating..."
|
|
|
436
436
|
ai_helper_admin_version_update_success: "Update Successful"
|
|
437
437
|
ai_helper_admin_version_update_failed_title: "Update Failed"
|
|
438
438
|
ai_helper_admin_version_notice_title: "Important Notes"
|
|
439
|
-
ai_helper_admin_version_notice_1: "Update will execute git pull, npm run build, and pm2 restart"
|
|
439
|
+
ai_helper_admin_version_notice_1: "Update will execute git pull, npm run build:plugin, and pm2 restart"
|
|
440
440
|
ai_helper_admin_version_notice_2: "HydroOJ service will briefly restart, causing a few seconds of downtime"
|
|
441
441
|
ai_helper_admin_version_notice_3: "Please ensure no critical operations are in progress"
|
|
442
442
|
ai_helper_admin_version_plugin_path: "Plugin path"
|
package/locales/zh.yaml
CHANGED
|
@@ -436,7 +436,7 @@ ai_helper_admin_version_updating: "正在更新..."
|
|
|
436
436
|
ai_helper_admin_version_update_success: "更新成功"
|
|
437
437
|
ai_helper_admin_version_update_failed_title: "更新失败"
|
|
438
438
|
ai_helper_admin_version_notice_title: "注意事项"
|
|
439
|
-
ai_helper_admin_version_notice_1: "更新将执行 git pull、npm run build 和 pm2 restart"
|
|
439
|
+
ai_helper_admin_version_notice_1: "更新将执行 git pull、npm run build:plugin 和 pm2 restart"
|
|
440
440
|
ai_helper_admin_version_notice_2: "HydroOJ 服务将短暂重启,可能导致数秒服务中断"
|
|
441
441
|
ai_helper_admin_version_notice_3: "请确保当前没有重要操作正在进行"
|
|
442
442
|
ai_helper_admin_version_plugin_path: "插件路径"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hydro-ai-helper",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"description": "HydroOJ AI Learning Assistant - 一个教学优先的 AI 辅助学习插件",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"author": "Alture",
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
"learning-assistant"
|
|
23
23
|
],
|
|
24
24
|
"scripts": {
|
|
25
|
-
"build": "echo 'Skipping build for Vercel Functions'",
|
|
26
25
|
"build:plugin": "tsc",
|
|
27
26
|
"dev": "tsc --watch",
|
|
28
27
|
"lint": "eslint src --ext .ts,.tsx",
|
|
@@ -31,7 +30,7 @@
|
|
|
31
30
|
"dependencies": {
|
|
32
31
|
"@types/highlight.js": "^9.12.4",
|
|
33
32
|
"@types/markdown-it": "^14.1.2",
|
|
34
|
-
"axios": "^1.
|
|
33
|
+
"axios": "^1.15.0",
|
|
35
34
|
"chart.js": "^4.5.1",
|
|
36
35
|
"dompurify": "^3.3.2",
|
|
37
36
|
"highlight.js": "^11.11.1",
|
|
@@ -49,7 +48,6 @@
|
|
|
49
48
|
"@types/react-dom": "^17.0.0",
|
|
50
49
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
51
50
|
"@typescript-eslint/parser": "^6.0.0",
|
|
52
|
-
"@vercel/node": "^3.0.0",
|
|
53
51
|
"eslint": "^8.0.0",
|
|
54
52
|
"hydrooj": "^4.0.0",
|
|
55
53
|
"jest": "^30.2.0",
|