From cef1172fc1590a53984b464c46d7dd4b1744991f Mon Sep 17 00:00:00 2001 From: chyoo Date: Mon, 19 Oct 2026 13:45:50 +0900 Subject: [PATCH] feat: move AI agent configuration to settings and hide analytics menu --- scripts/test-ai-model-picker.cjs | 2 +- scripts/test-settings-navigation.cjs | 61 +++++ src/App.tsx | 6 +- src/pageMeta.ts | 2 +- src/pages/curriculum/CurriculumPage.tsx | 261 +------------------- src/pages/settings/SettingsPage.tsx | 308 +++++++++++++++++++++++- src/routes.ts | 5 +- 7 files changed, 379 insertions(+), 266 deletions(-) create mode 100644 scripts/test-settings-navigation.cjs diff --git a/scripts/test-ai-model-picker.cjs b/scripts/test-ai-model-picker.cjs index 1b70316..49916f0 100644 --- a/scripts/test-ai-model-picker.cjs +++ b/scripts/test-ai-model-picker.cjs @@ -5,7 +5,7 @@ const vm = require('node:vm'); const ts = require('typescript'); const React = require('react'); -const source = ts.createSourceFile('CurriculumPage.tsx', fs.readFileSync(require.resolve('../src/pages/curriculum/CurriculumPage.tsx'), 'utf8'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); +const source = ts.createSourceFile('SettingsPage.tsx', fs.readFileSync(require.resolve('../src/pages/settings/SettingsPage.tsx'), 'utf8'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); const constants = source.statements.filter(node => ts.isVariableStatement(node) && ['AI_MODEL_DEFAULTS', 'AI_MODEL_OPTIONS', 'CUSTOM_AI_MODEL'].includes(node.declarationList.declarations[0].name.getText(source))); function compile(code, globals = {}) { const exports = {}; diff --git a/scripts/test-settings-navigation.cjs b/scripts/test-settings-navigation.cjs new file mode 100644 index 0000000..6899808 --- /dev/null +++ b/scripts/test-settings-navigation.cjs @@ -0,0 +1,61 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const vm = require('node:vm'); +const ts = require('typescript'); +const React = require('react'); + +const read = path => fs.readFileSync(require.resolve(`../src/${path}`), 'utf8'); +function compile(source, globals = {}) { + const exports = {}; + const code = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX } }).outputText; + vm.runInNewContext(code, { exports, require, ...globals }); + return exports; +} +function elements(root, predicate) { + if (!React.isValidElement(root)) return []; + return [...(predicate(root) ? [root] : []), ...React.Children.toArray(root.props.children).flatMap(child => elements(child, predicate))]; +} +const source = ts.createSourceFile('SettingsPage.tsx', read('pages/settings/SettingsPage.tsx'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); +const page = source.statements.find(node => ts.isFunctionDeclaration(node) && node.name?.text === 'SettingsPage'); +const CourseAiSettings = () => null; +const { SettingsPage } = compile(page.getText(source), { CourseAiSettings, PageHeader: () => null, Bot: () => null }); +const props = { + session: { accessToken: 'test-token', user: { id: 'teacher' } }, + selectedCourseId: 'course-a', + courses: [{ id: 'course-a', title: 'A' }, { id: 'course-b', title: 'B' }], + onSelectCourse() {}, +}; + +test('settings remounts the credential draft when course or session changes', () => { + const form = input => elements(SettingsPage(input), node => node.type === CourseAiSettings)[0]; + const initial = form(props); + assert.equal(initial.props.selectedCourseId, 'course-a'); + assert.notEqual(initial.key, form({ ...props, selectedCourseId: 'course-b' }).key); + assert.notEqual(initial.key, form({ ...props, session: { ...props.session, accessToken: 'new-token' } }).key); +}); + +test('settings offers the global course selection and no credential form without a course', () => { + const selected = []; + const tree = SettingsPage({ ...props, onSelectCourse: id => selected.push(id) }); + const header = React.Children.toArray(tree.props.children)[0]; + const select = elements(header.props.action, node => node.type === 'select')[0]; + assert.equal(select.props.value, 'course-a'); + select.props.onChange({ target: { value: 'course-b' } }); + assert.deepEqual(selected, ['course-b']); + assert.equal(elements(SettingsPage({ ...props, selectedCourseId: '', courses: [] }), node => node.type === CourseAiSettings).length, 0); +}); + +test('curriculum no longer loads AI settings or offers an agent tab', () => { + const curriculum = read('pages/curriculum/CurriculumPage.tsx'); + assert.doesNotMatch(curriculum, /getCourseAiSetting|updateCourseAiSetting|handleSaveAiSetting|setActiveTab\('ai'\)/); + assert.match(read('App.tsx'), /