From 2a111d21c0ddf1b73beefef48afe787b90f752a3 Mon Sep 17 00:00:00 2001 From: txai-dev Date: Sat, 12 Sep 2026 23:39:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v1.3.0=20=E5=B7=AE=E5=BC=82=E5=8C=96?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E2=80=94=E2=80=94=E6=98=9F=E5=9B=BE=E7=82=B9?= =?UTF-8?q?=E4=BA=AE=E4=BD=93=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0 修复: - 详情页正文渲染(content 分段),71 条知识不再空白 - 问答页去登录墙:游客每日 3 次(IP 限流),登录后每日 5 次 - 修正问答配额为真正的每日重置语义 点亮闭环: - KnowledgeLit 模型 + 服务端判题(POST /:id/quiz,答案不下发) - /api/knowledge/stats 星域统计 + 点亮进度 + streak + litIds - AI 批量生成 71 条读后检测题(scripts/generate-quiz.js) 设计系统 2.0: - 70 个统一线性图标替换全部 emoji 图标 - 主题 token mixin 化,修复 H5 暗色级联 bug,MP 跟随系统(darkmode) - 首页星域导航(进度环)、我的页学习者档案、星域列表页重写 - 硬编码色清理,误植 map 页移除 服务端加固: - trust proxy 真实 IP、错误信息脱敏、ask 独立限流、无效 ObjectId 404 - seo.js ESM→CJS(修复容器启动)、AI 导师"维度精灵"人格化 --- client/src/App.vue | 270 ++---- client/src/components/AppIcon.vue | 30 + client/src/components/ProgressRing.vue | 63 ++ client/src/manifest.json | 7 +- client/src/pages.json | 26 +- .../src/pages/achievements/achievements.vue | 12 +- client/src/pages/chat/chat.vue | 476 +++++------ client/src/pages/detail/detail.vue | 651 ++++++++------- client/src/pages/dimension/dimension.vue | 382 ++++----- client/src/pages/favorites/favorites.vue | 2 +- client/src/pages/feedback/feedback.vue | 4 +- client/src/pages/index/index.vue | 440 +++++----- client/src/pages/map/map.vue | 106 --- client/src/pages/map/quiz.vue | 89 -- client/src/pages/news/news.vue | 5 +- client/src/pages/profile/profile.vue | 781 ++++++++++-------- client/src/pages/search/search.vue | 4 +- client/src/pages/settings/settings.vue | 2 +- client/src/pages/trend/trend.vue | 16 +- client/src/static/icons/arrow-gray.png | Bin 0 -> 989 bytes client/src/static/icons/arrow-white.png | Bin 0 -> 496 bytes client/src/static/icons/arrow.png | Bin 0 -> 1007 bytes client/src/static/icons/back-gray.png | Bin 0 -> 859 bytes client/src/static/icons/back-white.png | Bin 0 -> 448 bytes client/src/static/icons/back.png | Bin 0 -> 874 bytes client/src/static/icons/book-gray.png | Bin 0 -> 1336 bytes client/src/static/icons/book-white.png | Bin 0 -> 1066 bytes client/src/static/icons/book.png | Bin 0 -> 1355 bytes client/src/static/icons/bulb-gray.png | Bin 0 -> 1477 bytes client/src/static/icons/bulb-white.png | Bin 0 -> 1026 bytes client/src/static/icons/bulb.png | Bin 0 -> 1454 bytes client/src/static/icons/chat-gray.png | Bin 0 -> 1213 bytes client/src/static/icons/chat-white.png | Bin 0 -> 934 bytes client/src/static/icons/chat.png | Bin 297 -> 1179 bytes client/src/static/icons/check-gray.png | Bin 0 -> 1178 bytes client/src/static/icons/check-green.png | Bin 0 -> 1153 bytes client/src/static/icons/check-white.png | Bin 0 -> 660 bytes client/src/static/icons/check.png | Bin 0 -> 1099 bytes client/src/static/icons/clock-gray.png | Bin 0 -> 2469 bytes client/src/static/icons/clock-white.png | Bin 0 -> 1538 bytes client/src/static/icons/clock.png | Bin 0 -> 2426 bytes client/src/static/icons/feedback-gray.png | Bin 0 -> 1298 bytes client/src/static/icons/feedback-white.png | Bin 0 -> 984 bytes client/src/static/icons/feedback.png | Bin 0 -> 1271 bytes client/src/static/icons/flame-gray.png | Bin 0 -> 2877 bytes client/src/static/icons/flame-white.png | Bin 0 -> 1698 bytes client/src/static/icons/flame.png | Bin 0 -> 2844 bytes client/src/static/icons/gift-gray.png | Bin 0 -> 1595 bytes client/src/static/icons/gift-white.png | Bin 0 -> 1104 bytes client/src/static/icons/gift.png | Bin 0 -> 1534 bytes client/src/static/icons/lock-gray.png | Bin 0 -> 1029 bytes client/src/static/icons/lock-white.png | Bin 0 -> 737 bytes client/src/static/icons/lock.png | Bin 0 -> 1012 bytes client/src/static/icons/notice-gray.png | Bin 0 -> 1765 bytes client/src/static/icons/notice-white.png | Bin 0 -> 1208 bytes client/src/static/icons/notice.png | Bin 0 -> 1745 bytes client/src/static/icons/planet-gray.png | Bin 0 -> 3004 bytes client/src/static/icons/planet-white.png | Bin 0 -> 1797 bytes client/src/static/icons/planet.png | Bin 0 -> 2978 bytes client/src/static/icons/rocket-gray.png | Bin 0 -> 3269 bytes client/src/static/icons/rocket-white.png | Bin 0 -> 1834 bytes client/src/static/icons/rocket.png | Bin 0 -> 3180 bytes client/src/static/icons/search-gray.png | Bin 0 -> 1979 bytes client/src/static/icons/search-white.png | Bin 0 -> 1268 bytes client/src/static/icons/search.png | Bin 0 -> 1981 bytes client/src/static/icons/send-gray.png | Bin 0 -> 1958 bytes client/src/static/icons/send-white.png | Bin 0 -> 1070 bytes client/src/static/icons/send.png | Bin 0 -> 1931 bytes client/src/static/icons/settings-gray.png | Bin 0 -> 2394 bytes client/src/static/icons/settings-white.png | Bin 0 -> 1515 bytes client/src/static/icons/settings.png | Bin 0 -> 2356 bytes client/src/static/icons/share-gray.png | Bin 0 -> 1829 bytes client/src/static/icons/share-white.png | Bin 0 -> 1306 bytes client/src/static/icons/share.png | Bin 0 -> 1829 bytes client/src/static/icons/sparkle-gray.png | Bin 0 -> 2712 bytes client/src/static/icons/sparkle-white.png | Bin 0 -> 1584 bytes client/src/static/icons/sparkle.png | Bin 0 -> 2721 bytes client/src/static/icons/star-fill-gray.png | Bin 0 -> 1647 bytes client/src/static/icons/star-fill-white.png | Bin 0 -> 1116 bytes client/src/static/icons/star-fill.png | Bin 0 -> 1600 bytes client/src/static/icons/star-gray.png | Bin 0 -> 2511 bytes client/src/static/icons/star-white.png | Bin 0 -> 1436 bytes client/src/static/icons/star.png | Bin 0 -> 2427 bytes client/src/static/icons/trophy-gray.png | Bin 0 -> 2254 bytes client/src/static/icons/trophy-white.png | Bin 0 -> 1472 bytes client/src/static/icons/trophy.png | Bin 0 -> 2220 bytes client/src/static/icons/user-gray.png | Bin 0 -> 2052 bytes client/src/static/icons/user-white.png | Bin 0 -> 1278 bytes client/src/static/icons/user.png | Bin 0 -> 2036 bytes client/src/styles/theme.scss | 167 +++- client/src/theme.json | 20 + client/src/utils/api.js | 12 +- client/src/utils/dimensions.js | 43 +- deploy/server.Dockerfile | 14 +- docker-compose.yml | 2 + scripts/gen-icons.mjs | 212 +++++ server/src/index.js | 28 +- server/src/models/AIChatQuota.js | 11 +- server/src/models/KnowledgeLit.js | 38 + server/src/models/index.js | 2 + server/src/routes/aiChat.js | 233 +++--- server/src/routes/knowledge.js | 565 ++++++++----- server/src/routes/seo.js | 19 +- server/src/scripts/generate-quiz.js | 153 ++++ 104 files changed, 2776 insertions(+), 2109 deletions(-) create mode 100644 client/src/components/AppIcon.vue create mode 100644 client/src/components/ProgressRing.vue delete mode 100644 client/src/pages/map/map.vue delete mode 100644 client/src/pages/map/quiz.vue create mode 100644 client/src/static/icons/arrow-gray.png create mode 100644 client/src/static/icons/arrow-white.png create mode 100644 client/src/static/icons/arrow.png create mode 100644 client/src/static/icons/back-gray.png create mode 100644 client/src/static/icons/back-white.png create mode 100644 client/src/static/icons/back.png create mode 100644 client/src/static/icons/book-gray.png create mode 100644 client/src/static/icons/book-white.png create mode 100644 client/src/static/icons/book.png create mode 100644 client/src/static/icons/bulb-gray.png create mode 100644 client/src/static/icons/bulb-white.png create mode 100644 client/src/static/icons/bulb.png create mode 100644 client/src/static/icons/chat-gray.png create mode 100644 client/src/static/icons/chat-white.png create mode 100644 client/src/static/icons/check-gray.png create mode 100644 client/src/static/icons/check-green.png create mode 100644 client/src/static/icons/check-white.png create mode 100644 client/src/static/icons/check.png create mode 100644 client/src/static/icons/clock-gray.png create mode 100644 client/src/static/icons/clock-white.png create mode 100644 client/src/static/icons/clock.png create mode 100644 client/src/static/icons/feedback-gray.png create mode 100644 client/src/static/icons/feedback-white.png create mode 100644 client/src/static/icons/feedback.png create mode 100644 client/src/static/icons/flame-gray.png create mode 100644 client/src/static/icons/flame-white.png create mode 100644 client/src/static/icons/flame.png create mode 100644 client/src/static/icons/gift-gray.png create mode 100644 client/src/static/icons/gift-white.png create mode 100644 client/src/static/icons/gift.png create mode 100644 client/src/static/icons/lock-gray.png create mode 100644 client/src/static/icons/lock-white.png create mode 100644 client/src/static/icons/lock.png create mode 100644 client/src/static/icons/notice-gray.png create mode 100644 client/src/static/icons/notice-white.png create mode 100644 client/src/static/icons/notice.png create mode 100644 client/src/static/icons/planet-gray.png create mode 100644 client/src/static/icons/planet-white.png create mode 100644 client/src/static/icons/planet.png create mode 100644 client/src/static/icons/rocket-gray.png create mode 100644 client/src/static/icons/rocket-white.png create mode 100644 client/src/static/icons/rocket.png create mode 100644 client/src/static/icons/search-gray.png create mode 100644 client/src/static/icons/search-white.png create mode 100644 client/src/static/icons/search.png create mode 100644 client/src/static/icons/send-gray.png create mode 100644 client/src/static/icons/send-white.png create mode 100644 client/src/static/icons/send.png create mode 100644 client/src/static/icons/settings-gray.png create mode 100644 client/src/static/icons/settings-white.png create mode 100644 client/src/static/icons/settings.png create mode 100644 client/src/static/icons/share-gray.png create mode 100644 client/src/static/icons/share-white.png create mode 100644 client/src/static/icons/share.png create mode 100644 client/src/static/icons/sparkle-gray.png create mode 100644 client/src/static/icons/sparkle-white.png create mode 100644 client/src/static/icons/sparkle.png create mode 100644 client/src/static/icons/star-fill-gray.png create mode 100644 client/src/static/icons/star-fill-white.png create mode 100644 client/src/static/icons/star-fill.png create mode 100644 client/src/static/icons/star-gray.png create mode 100644 client/src/static/icons/star-white.png create mode 100644 client/src/static/icons/star.png create mode 100644 client/src/static/icons/trophy-gray.png create mode 100644 client/src/static/icons/trophy-white.png create mode 100644 client/src/static/icons/trophy.png create mode 100644 client/src/static/icons/user-gray.png create mode 100644 client/src/static/icons/user-white.png create mode 100644 client/src/static/icons/user.png create mode 100644 client/src/theme.json create mode 100644 scripts/gen-icons.mjs create mode 100644 server/src/models/KnowledgeLit.js create mode 100644 server/src/scripts/generate-quiz.js diff --git a/client/src/App.vue b/client/src/App.vue index 91a03f3..f5e5729 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -8,39 +8,44 @@ import { onLaunch, onShow, onHide } from '@dcloudio/uni-app' onLaunch(() => { - console.log('[AI维度] App launched') initTheme() }) -onShow(() => { - console.log('[AI维度] App shown') -}) - -onHide(() => { - console.log('[AI维度] App hidden') -}) +onShow(() => {}) +onHide(() => {}) +// 主题:'auto'(默认,跟随系统)| 'light' | 'dark' function initTheme() { - const theme = uni.getStorageSync('theme') || 'light' + const theme = uni.getStorageSync('theme') || 'auto' applyTheme(theme) + // auto 模式下监听系统切换(H5) + if (theme === 'auto' && typeof window !== 'undefined' && window.matchMedia) { + const mq = window.matchMedia('(prefers-color-scheme: dark)') + const onChange = () => applyTheme('auto') + if (mq.addEventListener) mq.addEventListener('change', onChange) + else if (mq.addListener) mq.addListener(onChange) + } +} + +function resolveTheme(theme) { + if (theme !== 'auto') return theme + if (typeof window !== 'undefined' && window.matchMedia) { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' + } + return 'light' } function applyTheme(theme) { + const resolved = resolveTheme(theme) try { // H5 环境:操作 document class const root = document.documentElement - if (theme === 'dark') { - root.classList.add('theme-dark') - root.classList.remove('theme-light') - } else { - root.classList.remove('theme-dark') - root.classList.add('theme-light') - } + root.classList.remove('theme-dark', 'theme-light') + root.classList.add(resolved === 'dark' ? 'theme-dark' : 'theme-light') } catch (e) { - // 小程序环境:document 不存在 + // 小程序环境:document 不存在,深色由 manifest darkmode + CSS 媒体查询接管 } - // 小程序 tabBar 原生组件同步切换 - applyTabBarTheme(theme) + applyTabBarTheme(resolved) } function applyTabBarTheme(theme) { @@ -48,202 +53,34 @@ function applyTabBarTheme(theme) { if (theme === 'dark') { uni.setTabBarStyle({ color: 'rgba(255,255,255,0.35)', - selectedColor: '#a78bfa', - backgroundColor: '#0d0d1a', + selectedColor: '#a2b2ff', + backgroundColor: '#181828', borderStyle: 'black' }) } else { uni.setTabBarStyle({ color: 'rgba(0,0,0,0.3)', - selectedColor: '#6366f1', + selectedColor: '#5b5ff8', backgroundColor: '#ffffff', borderStyle: 'white' }) } } catch (e) { - // H5 环境 setTabBarStyle 可能不支持 + // 非 tabBar 页面或不支持时忽略 } } - \ No newline at end of file + +/* 区块标题(统一) */ +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin: 24px 0 12px; + + .section-title { + font-size: 16px; + font-weight: 700; + color: var(--text-primary); + display: flex; + align-items: center; + gap: 6px; + } + .section-more { + font-size: 12px; + color: var(--text-tertiary); + } +} + diff --git a/client/src/components/AppIcon.vue b/client/src/components/AppIcon.vue new file mode 100644 index 0000000..711d016 --- /dev/null +++ b/client/src/components/AppIcon.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/client/src/components/ProgressRing.vue b/client/src/components/ProgressRing.vue new file mode 100644 index 0000000..7bf0b75 --- /dev/null +++ b/client/src/components/ProgressRing.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/client/src/manifest.json b/client/src/manifest.json index afc376d..d5a6c89 100644 --- a/client/src/manifest.json +++ b/client/src/manifest.json @@ -17,6 +17,9 @@ }, "usingComponents": true, "permission": {}, - "requiredPrivateInfos": [] - } + "requiredPrivateInfos": [], + "darkmode": true, + "themeLocation": "theme.json" + }, + "app-plus": {} } \ No newline at end of file diff --git a/client/src/pages.json b/client/src/pages.json index 60619f4..39ed0e1 100644 --- a/client/src/pages.json +++ b/client/src/pages.json @@ -124,25 +124,13 @@ "navigationBarTitleText": "协议", "navigationStyle": "custom" } - }, - { - "path": "pages/map/map", - "style": { - "navigationBarTitleText": "AI 知识地图" - } - }, - { - "path": "pages/map/quiz", - "style": { - "navigationBarTitleText": "知识闯关" - } } ], "globalStyle": { - "navigationBarTextStyle": "black", + "navigationBarTextStyle": "@navTxt", "navigationBarTitleText": "宇之然AI维度", - "navigationBarBackgroundColor": "#f5f5f7", - "backgroundColor": "#f5f5f7", + "navigationBarBackgroundColor": "@navBg", + "backgroundColor": "@bg", "backgroundColorTop": "#f5f5f7", "backgroundColorBottom": "#f5f5f7", "app-plus": { @@ -150,10 +138,10 @@ } }, "tabBar": { - "color": "rgba(0,0,0,0.3)", - "selectedColor": "#6366f1", - "backgroundColor": "#ffffff", - "borderStyle": "white", + "color": "@tabTxt", + "selectedColor": "@tabSel", + "backgroundColor": "@tabBg", + "borderStyle": "@tabBorder", "list": [ { "pagePath": "pages/index/index", diff --git a/client/src/pages/achievements/achievements.vue b/client/src/pages/achievements/achievements.vue index 3d452df..edc270e 100644 --- a/client/src/pages/achievements/achievements.vue +++ b/client/src/pages/achievements/achievements.vue @@ -4,7 +4,7 @@ - 🏆 成就 + 成就 {{ unlockedCount }}/{{ totalCount }} @@ -62,7 +62,7 @@ - 🏆 + 暂无成就 还没有获得任何成就 继续学习,解锁属于你的成就吧 @@ -88,9 +88,9 @@ const loading = ref(true) // 成就分类的默认配置 const CATEGORY_CONFIG = [ - { name: '知识探索', icon: '📖', color: 'var(--dim1-color)' }, - { name: '收藏成就', icon: '⭐', color: 'var(--dim2-color)' }, - { name: 'AI 问答', icon: '🤖', color: 'var(--dim3-color)' }, + { name: '知识探索', icon: '星', color: 'var(--dim1-color)' }, + { name: '收藏成就', icon: '藏', color: 'var(--dim2-color)' }, + { name: 'AI 问答', icon: '问', color: 'var(--dim3-color)' }, { name: '社交成就', icon: '👥', color: 'var(--dim4-color)' } ] @@ -141,7 +141,7 @@ async function loadAchievements() { function catIcon(name) { const config = CATEGORY_CONFIG.find((c) => c.name === name) - return config ? config.icon : '🏆' + return config ? config.icon : '成' } function achColor(icon) { diff --git a/client/src/pages/chat/chat.vue b/client/src/pages/chat/chat.vue index 084ea86..3cd98af 100644 --- a/client/src/pages/chat/chat.vue +++ b/client/src/pages/chat/chat.vue @@ -2,28 +2,29 @@ - + - ∞ + - AI 助手 - 随时为你解答 + 维度精灵 + 你的 AI 知识探索向导 - - - {{ quotaDisplay }} - 🎁 + + + {{ quotaText }} - + - + - 💡 - 试试问我这些问题: + + + {{ welcomeText }} + {{ item }} @@ -32,7 +33,9 @@ - ∞ + + + @@ -40,7 +43,9 @@ - ∞ + + + @@ -49,40 +54,42 @@ + + - + - + + - - ➤ - - - - @@ -212,7 +238,7 @@ onShareAppMessage(() => ({ height: 100vh; display: flex; flex-direction: column; - padding: 0 20px; + padding: 0 16px; background: var(--bg-primary); } @@ -222,248 +248,230 @@ onShareAppMessage(() => ({ display: flex; align-items: center; gap: 12px; - padding: 10px 0 16px; + padding: 10px 0 14px; } .chat-avatar, .mini-avatar { - width: 42px; - height: 42px; border-radius: 14px; - background: linear-gradient(135deg, rgba(124, 77, 255, 0.3), rgba(0, 188, 212, 0.2)); - border: 1px solid rgba(124, 77, 255, 0.15); + background: linear-gradient(135deg, var(--color-brand), #8b5cf6); display: flex; align-items: center; justify-content: center; flex-shrink: 0; + box-shadow: 0 4px 14px var(--color-brand-glow); } - -.avatar-text { - font-size: 20px; - color: white; -} - -.mini-avatar { - width: 32px; - height: 32px; - border-radius: 10px; - font-size: 14px; -} +.chat-avatar { width: 42px; height: 42px; } +.mini-avatar { width: 28px; height: 28px; border-radius: 9px; } .chat-info { flex: 1; } - .chat-name { - font-size: 16px; - color: var(--text-primary); - font-weight: 600; display: block; + font-size: 16px; + font-weight: 700; + color: var(--text-primary); } - .chat-status { + display: block; font-size: 11px; color: var(--text-tertiary); - display: block; + margin-top: 2px; } -// 配额徽章 .quota-badge { display: flex; align-items: center; - gap: 4px; + gap: 5px; padding: 6px 12px; - background: rgba(124, 77, 255, 0.1); - border: 1px solid rgba(124, 77, 255, 0.2); - border-radius: 16px; + border-radius: var(--radius-full); + background: var(--color-brand-bg); + border: 1px solid var(--glass-border); + + .quota-text { + font-size: 12px; + font-weight: 700; + color: var(--color-brand); + } } -.quota-text { - font-size: 12px; - color: var(--dim1-color); - font-weight: 600; +.msg-list { + flex: 1; + min-height: 0; } -.quota-icon { font-size: 14px; } +.list-bottom { height: 16px; } -// 未登录输入区占位 -.login-input-placeholder { - display: flex; - align-items: center; - justify-content: space-between; - padding: 14px 18px; - background: var(--glass-bg); - border: 1px dashed var(--glass-border); - border-radius: 14px; - margin-bottom: 30px; - cursor: pointer; -} - -.login-input-text { - font-size: 14px; - color: var(--text-tertiary); -} - -.login-input-btn { - padding: 6px 16px; - background: var(--color-brand); - border-radius: 8px; -} - -.login-input-btn text { - font-size: 13px; - color: white; - font-weight: 600; -} - -// 欢迎卡片 .welcome-card { background: var(--glass-bg); border: 1px solid var(--glass-border); - border-radius: 14px; + border-radius: var(--radius-lg); padding: 16px; margin-bottom: 16px; + box-shadow: var(--card-shadow, none); } +.welcome-head { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 12px; -.welcome-icon { - font-size: 20px; - display: block; - margin-bottom: 6px; + .welcome-text { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + line-height: 1.5; + } } - -.welcome-text { - font-size: 13px; - color: var(--text-tertiary); - display: block; - margin-bottom: 10px; -} - .suggest-list { display: flex; flex-direction: column; - gap: 6px; + gap: 8px; } - .suggest-item { - padding: 8px 12px; - background: rgba(124, 77, 255, 0.08); - border-radius: 8px; - font-size: 12px; + padding: 11px 14px; + border-radius: var(--radius-md); + background: var(--bg-primary); + border: 1px solid var(--glass-border); + font-size: 13px; color: var(--text-secondary); + line-height: 1.5; + transition: all 0.2s ease; } - -// 消息列表 -.msg-list { - flex: 1; - overflow-y: auto; - padding-bottom: 10px; -} +.suggest-item:active { border-color: var(--color-brand); color: var(--color-brand); } .chat-msg { display: flex; - gap: 10px; + gap: 8px; margin-bottom: 14px; -} -.chat-msg.user { - flex-direction: row-reverse; -} + &.assistant .msg-content { align-items: flex-start; } -.chat-msg.assistant { - flex-direction: row; -} + .msg-content { + max-width: 82%; + display: flex; + flex-direction: column; + } -.msg-content { - max-width: 82%; + &.user { + justify-content: flex-end; + + .msg-content { align-items: flex-end; } + .bubble { + background: linear-gradient(135deg, var(--color-brand), var(--color-brand-light)); + color: #fff; + border-radius: 16px 16px 4px 16px; + } + } + + &.assistant .bubble { + background: var(--glass-bg); + border: 1px solid var(--glass-border); + color: var(--text-primary); + border-radius: 16px 16px 16px 4px; + } } .bubble { - padding: 12px 16px; - border-radius: 16px; + padding: 12px 14px; font-size: 14px; - line-height: 1.6; - white-space: pre-wrap; + line-height: 1.7; + word-break: break-word; } -.chat-msg.assistant .bubble { - background: var(--glass-bg); - border: 1px solid var(--glass-border); - color: var(--text-primary); - border-bottom-left-radius: 4px; -} - -.chat-msg.user .bubble { - background: linear-gradient(135deg, rgba(124, 77, 255, 0.3), rgba(124, 77, 255, 0.15)); - border: 1px solid rgba(124, 77, 255, 0.15); - color: white; - border-bottom-right-radius: 4px; -} - -// 加载动画 .loading-dots { display: flex; - gap: 4px; - padding: 14px 18px; + gap: 5px; + align-items: center; + padding: 16px 14px; + + .dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--color-brand); + animation: dotBounce 1.2s infinite ease-in-out; + + &:nth-child(2) { animation-delay: 0.15s; } + &:nth-child(3) { animation-delay: 0.3s; } + } } -.dot { - width: 6px; - height: 6px; - border-radius: 50%; - background: rgba(255, 255, 255, 0.4); - animation: blink 1.4s infinite; +@keyframes dotBounce { + 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; } + 30% { transform: translateY(-5px); opacity: 1; } } -.dot:nth-child(2) { animation-delay: 0.2s; } -.dot:nth-child(3) { animation-delay: 0.4s; } - -@keyframes blink { - 0%, 20% { opacity: 0; transform: scale(0.6); } - 50% { opacity: 1; transform: scale(1); } - 100% { opacity: 0; transform: scale(0.6); } -} - -// 输入区 +/* 输入区 */ .chat-input-area { - padding: 12px 0 30px; - background: var(--bg-primary); + flex-shrink: 0; + padding: 10px 0 12px; +} + +.guest-hint { + display: block; + text-align: center; + font-size: 11px; + color: var(--text-muted); + margin-top: 8px; } .chat-input { - background: var(--glass-bg); - border: 1px solid var(--glass-border); - border-radius: 14px; - padding: 10px 14px; display: flex; align-items: center; gap: 10px; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + border-radius: var(--radius-full); + padding: 6px 6px 6px 18px; } .input-field { flex: 1; - height: 24px; + height: 38px; font-size: 14px; color: var(--text-primary); - background: none; - border: none; - outline: none; -} - -.input-field::placeholder { - color: var(--text-muted); + background: transparent; } .send-btn { - width: 36px; - height: 36px; + width: 38px; + height: 38px; border-radius: 50%; - background: linear-gradient(135deg, #7c4dff, #b388ff); + background: linear-gradient(135deg, var(--color-brand), var(--color-brand-light)); display: flex; align-items: center; justify-content: center; - color: white; - box-shadow: 0 0 20px rgba(124, 77, 255, 0.2); + flex-shrink: 0; + transition: opacity 0.2s ease; + + &.disabled { opacity: 0.45; } } -.send-btn text { - font-size: 16px; +/* 用完额度引导 */ +.login-guide { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 14px 16px; + border-radius: var(--radius-lg); + background: linear-gradient(135deg, var(--color-brand-bg), rgba(139, 92, 246, 0.06)); + border: 1px solid var(--glass-hover-border); + + .guide-text { + flex: 1; + font-size: 12px; + color: var(--text-secondary); + line-height: 1.6; + } + .guide-btn { + padding: 8px 14px; + border-radius: var(--radius-full); + background: linear-gradient(135deg, var(--color-brand), var(--color-brand-light)); + color: #fff; + font-size: 12px; + font-weight: 600; + flex-shrink: 0; + } } diff --git a/client/src/pages/detail/detail.vue b/client/src/pages/detail/detail.vue index e821aae..f507536 100644 --- a/client/src/pages/detail/detail.vue +++ b/client/src/pages/detail/detail.vue @@ -1,29 +1,35 @@ diff --git a/client/src/pages/dimension/dimension.vue b/client/src/pages/dimension/dimension.vue index 26eeec3..aa9a842 100644 --- a/client/src/pages/dimension/dimension.vue +++ b/client/src/pages/dimension/dimension.vue @@ -2,72 +2,96 @@ - - ← 返回 + + + + 返回 + - + - {{ currentDim?.icon }} - {{ currentDim?.name }} - {{ bannerDesc }} - - + - 探索进度 {{ progress }}% · 已完成 {{ readCount }}/{{ totalCount }} 篇 + {{ litCount }}/{{ totalCount }} 颗星球已点亮 · {{ litPercent }}% - - - - {{ item.icon || '📄' }} + + + + - - {{ item.title }} - {{ item.desc }} - - 📖 {{ item.readTime || 5 }} 分钟 - ⭐ 已收藏 - 🔖 {{ item.collectCount || 0 }} 人收藏 - - {{ item.isPremium ? 'Pro' : '免费' }} - + + {{ item.title }} + {{ item.desc }} + + 已点亮 + {{ item.readTime }} 分钟 + 含检测题 + + + + + + + 这片星域还没有星球 + 内容正在扩充中,先去别的星域逛逛 - 加载中... + + @@ -185,107 +154,148 @@ onShareAppMessage(() => ({ .status-bar-placeholder { height: 44px; } +.nav-row { padding-bottom: 12px; } .back-btn { - color: rgba(255,255,255,0.4); - font-size: 14px; - display: flex; + display: inline-flex; align-items: center; - gap: 6px; - padding: 10px 0 14px; + gap: 4px; + padding: 8px 14px 8px 10px; + border-radius: var(--radius-full); + background: var(--glass-bg); + border: 1px solid var(--glass-border); + font-size: 13px; + color: var(--text-secondary); } .dim-banner { - border-radius: 18px; - padding: 24px 20px; - margin-bottom: 20px; - background: linear-gradient(135deg, rgba(124,77,255,0.1), rgba(0,0,0,0.3)); - border: 1px solid rgba(124,77,255,0.12); - position: relative; - overflow: hidden; -} -.dim-banner-icon { font-size: 28px; display: block; margin-bottom: 8px; } -.dim-banner-title { font-size: 22px; color: var(--text-primary); display: block; margin-bottom: 6px; } -.dim-banner-desc { - font-size: 12px; - color: rgba(255,255,255,0.4); - line-height: 1.6; - display: block; + border-radius: var(--radius-lg); + padding: 18px 16px; + margin-bottom: 16px; + background: var(--glass-bg); + border: 1.5px solid var(--glass-border); + box-shadow: var(--card-shadow, none); } -.progress-track { - height: 3px; - background: var(--glass-bg); - border-radius: 2px; - margin-top: 16px; - overflow: hidden; -} -.progress-fill { - height: 100%; - background: linear-gradient(90deg, #b388ff, #7c4dff); - border-radius: 2px; -} -.progress-label { - font-size: 11px; - color: rgba(255,255,255,0.2); - margin-top: 8px; - display: block; -} - -.knowledge-item { - border-radius: 14px; - padding: 16px; - margin-bottom: 10px; - background: var(--glass-bg); - border: 1px solid var(--glass-border); +.banner-top { display: flex; - gap: 14px; - align-items: flex-start; + align-items: center; + gap: 12px; + margin-bottom: 12px; } - -.k-icon { +.dim-icon { width: 44px; height: 44px; - border-radius: 12px; - flex-shrink: 0; + border-radius: 14px; display: flex; align-items: center; justify-content: center; - font-size: 18px; - border: 1px solid var(--glass-border); + flex-shrink: 0; } -.k-info { flex: 1; } -.k-title { - font-size: 14px; - color: rgba(255,255,255,0.85); - font-weight: 500; +.banner-info { flex: 1; min-width: 0; } +.dim-banner-title { display: block; - margin-bottom: 4px; + font-size: 20px; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 3px; } -.k-desc { - font-size: 11px; - color: rgba(255,255,255,0.3); +.dim-banner-desc { + font-size: 12px; + color: var(--text-tertiary); line-height: 1.5; - margin-bottom: 8px; - display: block; } -.k-meta { +.progress-label { + font-size: 11px; + color: var(--text-tertiary); +} + +.planet-item { display: flex; + align-items: center; gap: 12px; + border-radius: var(--radius-lg); + padding: 14px; + margin-bottom: 10px; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + box-shadow: var(--card-shadow, none); + transition: transform 0.2s ease; +} +.planet-item:active { transform: scale(0.98); } + +.p-icon { + width: 40px; height: 40px; + border-radius: 13px; + border: 1.5px dashed var(--glass-border); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + &.lit { border-style: solid; } +} + +.p-info { flex: 1; min-width: 0; } +.p-title { + display: block; + font-size: 14px; + color: var(--text-primary); + font-weight: 600; + line-height: 1.5; + margin-bottom: 3px; +} +.p-desc { + display: block; + font-size: 11px; + color: var(--text-tertiary); + line-height: 1.5; + margin-bottom: 6px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.p-meta { + display: flex; + gap: 8px; align-items: center; font-size: 10px; - color: rgba(255,255,255,0.15); + color: var(--text-muted); } -.k-badge { - font-size: 10px; - padding: 2px 10px; - border-radius: 6px; - font-weight: 500; +.p-lit { + font-weight: 600; + padding: 2px 8px; + border-radius: var(--radius-full); +} +.p-quiz { + padding: 2px 8px; + border-radius: var(--radius-full); + background: var(--bg-tertiary); +} + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + padding: 50px 0 30px; + gap: 8px; + + .empty-title { + font-size: 15px; + font-weight: 600; + color: var(--text-secondary); + margin-top: 8px; + } + .empty-desc { + font-size: 12px; + color: var(--text-muted); + } } -.k-badge.free { background: rgba(76,175,80,0.12); color: #66bb6a; } -.k-badge.pro { background: rgba(255,152,0,0.12); color: #ffa726; } .loading-state { text-align: center; padding: 40px 0; - color: rgba(255,255,255,0.2); + color: var(--text-muted); + font-size: 13px; } - \ No newline at end of file + +.bottom-spacer { height: 20px; } + diff --git a/client/src/pages/favorites/favorites.vue b/client/src/pages/favorites/favorites.vue index 715d337..1ed5876 100644 --- a/client/src/pages/favorites/favorites.vue +++ b/client/src/pages/favorites/favorites.vue @@ -34,7 +34,7 @@ - ⭐ + 还没有收藏 你还没有收藏任何内容 登录后可收藏喜欢的 AI 知识 去首页发现有趣的 AI 知识吧 diff --git a/client/src/pages/feedback/feedback.vue b/client/src/pages/feedback/feedback.vue index 835e293..b62a225 100644 --- a/client/src/pages/feedback/feedback.vue +++ b/client/src/pages/feedback/feedback.vue @@ -112,7 +112,7 @@ const userStore = useUserStore() // 反馈类型 const feedbackTypes = [ { value: 'bug', name: '问题报告', icon: '🐛' }, - { value: 'feature', name: '功能建议', icon: '💡' }, + { value: 'feature', name: '功能建议', icon: '★' }, { value: 'content', name: '内容纠错', icon: '📝' }, { value: 'other', name: '其他', icon: '💬' } ] @@ -147,7 +147,7 @@ function statusLabel(status) { function typeLabel(type) { const map = { bug: '🐛 问题报告', - feature: '💡 功能建议', + feature: '功能建议', content: '📝 内容纠错', other: '💬 其他' } diff --git a/client/src/pages/index/index.vue b/client/src/pages/index/index.vue index 05b8f68..a68936f 100644 --- a/client/src/pages/index/index.vue +++ b/client/src/pages/index/index.vue @@ -1,75 +1,93 @@ @@ -178,15 +236,13 @@ onShareAppMessage(() => ({ background: var(--bg-primary); } -.status-bar-placeholder { - height: 44px; -} +.status-bar-placeholder { height: 44px; } .header { display: flex; justify-content: space-between; align-items: center; - padding: 6px 0 20px; + padding: 6px 0 18px; } .brand { @@ -196,38 +252,32 @@ onShareAppMessage(() => ({ } .brand-icon { - width: 36px; - height: 36px; - border-radius: 10px; - background: linear-gradient(135deg, rgba(124,77,255,0.3), rgba(0,188,212,0.2)); - border: 1px solid rgba(124,77,255,0.2); + width: 38px; + height: 38px; + border-radius: 12px; + background: linear-gradient(135deg, var(--color-brand), #8b5cf6); display: flex; align-items: center; justify-content: center; - font-size: 18px; - box-shadow: 0 0 20px rgba(124,77,255,0.1); + box-shadow: 0 4px 14px var(--color-brand-glow); } -.brand-text { - display: flex; - flex-direction: column; -} +.brand-text { display: flex; flex-direction: column; } .brand-text .title { font-size: 20px; color: var(--text-primary); font-weight: 700; letter-spacing: 1px; + line-height: 1.2; } .brand-text .subtitle { font-size: 11px; - color: var(--text-muted); - letter-spacing: 2px; + color: var(--text-tertiary); + letter-spacing: 1.5px; + margin-top: 2px; } -.header-actions { - display: flex; - gap: 10px; -} +.header-actions { display: flex; gap: 10px; } .icon-btn { width: 36px; height: 36px; border-radius: 50%; @@ -236,167 +286,185 @@ onShareAppMessage(() => ({ display: flex; align-items: center; justify-content: center; - font-size: 15px; } -.greeting { - margin-bottom: 20px; - padding: 24px 20px; - border-radius: 18px; - background: linear-gradient(135deg, rgba(99,102,241,0.08), rgba(139,92,246,0.04)); - border: 1px solid rgba(99,102,241,0.1); +/* streak */ +.streak-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + border-radius: var(--radius-md); + background: linear-gradient(135deg, rgba(229, 28, 28, 0.08), rgba(223, 119, 0, 0.06)); + border: 1px solid rgba(229, 28, 28, 0.15); + margin-bottom: 12px; } -.greeting-title { - font-size: 26px; - color: var(--text-primary); - font-weight: 700; -} -.greeting-highlight { - font-size: 26px; - font-weight: 700; - background: linear-gradient(135deg, #b388ff, #7c4dff); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} -.greeting-desc { - display: block; +.streak-text { font-size: 13px; + font-weight: 700; + color: var(--dim5-color); +} +.streak-sub { + flex: 1; + font-size: 11px; color: var(--text-tertiary); - margin-top: 4px; +} +.streak-go { + width: 22px; height: 22px; + border-radius: 50%; + background: var(--dim5-color); + display: flex; + align-items: center; + justify-content: center; } -.search-bar { +/* 继续探索 */ +.resume-card { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; + border-radius: var(--radius-lg); background: var(--glass-bg); border: 1px solid var(--glass-border); - border-radius: 14px; - padding: 12px 16px; + box-shadow: var(--card-shadow, none); + margin-bottom: 12px; +} +.resume-icon { + width: 38px; height: 38px; + border-radius: 12px; + background: var(--color-brand-bg); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} +.resume-info { flex: 1; min-width: 0; } +.resume-label { + font-size: 11px; + color: var(--text-tertiary); + display: block; + margin-bottom: 2px; +} +.resume-title { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; +} + +/* 搜索 */ +.search-bar { display: flex; align-items: center; gap: 10px; - margin-bottom: 24px; - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); + padding: 12px 16px; + border-radius: var(--radius-full); + background: var(--glass-bg); + border: 1px solid var(--glass-border); + margin-bottom: 6px; } -.search-icon { color: var(--text-muted); } -.search-placeholder { color: var(--text-muted); font-size: 14px; } - -.section-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 14px; - padding: 8px 12px; - border-radius: 10px; - background: rgba(99,102,241,0.04); -} -.section-title { - font-size: 17px; - color: var(--text-primary); - font-weight: 600; - letter-spacing: 0.5px; -} -.section-more { - font-size: 12px; +.search-placeholder { + font-size: 13px; color: var(--text-muted); } +/* 星域网格 */ .dim-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; - margin-bottom: 28px; } - .dim-card { - border-radius: 18px; - padding: 20px 16px; + background: var(--glass-bg); border: 1px solid var(--glass-border); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); -} -.dim-full { - grid-column: 1 / -1; + border-radius: var(--radius-lg); + padding: 14px; + box-shadow: var(--card-shadow, none); + transition: transform 0.2s ease; } +.dim-card:active { transform: scale(0.97); } +.dim-top { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} .dim-icon { - width: 42px; height: 42px; - border-radius: 12px; + width: 34px; height: 34px; + border-radius: 11px; display: flex; align-items: center; justify-content: center; - font-size: 20px; - margin-bottom: 14px; } - .dim-name { - font-size: 15px; - font-weight: 600; - margin-bottom: 4px; - letter-spacing: 0.5px; display: block; + font-size: 15px; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 3px; } .dim-desc { - font-size: 12px; + display: block; + font-size: 11px; color: var(--text-tertiary); line-height: 1.5; - display: block; + margin-bottom: 10px; +} +.dim-foot { + display: flex; + align-items: center; + justify-content: space-between; +} +.dim-lit { + font-size: 10px; + font-weight: 600; + padding: 3px 8px; + border-radius: var(--radius-full); } .dim-count { - font-size: 11px; + font-size: 10px; color: var(--text-muted); - margin-top: 10px; - display: block; } +/* 快讯 */ .news-card { - border-radius: 14px; - padding: 14px 16px; - margin-bottom: 10px; background: var(--glass-bg); border: 1px solid var(--glass-border); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); + border-radius: var(--radius-md); + padding: 14px 16px; + margin-bottom: 10px; + box-shadow: var(--card-shadow, none); } - .news-tag { display: inline-block; font-size: 10px; - padding: 2px 10px; - border-radius: 6px; + font-weight: 600; + padding: 2px 8px; + border-radius: var(--radius-full); margin-bottom: 8px; - font-weight: 500; } -.news-tag.hot { - background: rgba(239,83,80,0.12); - color: #ef5350; -} -.news-tag.new { - background: rgba(0,188,212,0.12); - color: #4dd0e1; -} -.news-tag.pick { - background: rgba(124,77,255,0.12); - color: #b388ff; -} - +.news-tag.hot { background: var(--color-hot-bg); color: var(--color-hot); } +.news-tag.new { background: var(--color-new-bg); color: var(--color-new); } +.news-tag.pick { background: var(--color-pick-bg); color: var(--color-pick); } .news-title { + display: block; font-size: 14px; + font-weight: 600; color: var(--text-primary); line-height: 1.5; margin-bottom: 8px; - font-weight: 500; - display: block; } - .news-meta { display: flex; - gap: 16px; + justify-content: space-between; font-size: 11px; color: var(--text-muted); } -.bottom-spacer { - height: 40px; -} - \ No newline at end of file +.bottom-spacer { height: 30px; } + diff --git a/client/src/pages/map/map.vue b/client/src/pages/map/map.vue deleted file mode 100644 index d6c85fe..0000000 --- a/client/src/pages/map/map.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - - - diff --git a/client/src/pages/map/quiz.vue b/client/src/pages/map/quiz.vue deleted file mode 100644 index 44e54bc..0000000 --- a/client/src/pages/map/quiz.vue +++ /dev/null @@ -1,89 +0,0 @@ - - - - - diff --git a/client/src/pages/news/news.vue b/client/src/pages/news/news.vue index 685f993..3c7dab4 100644 --- a/client/src/pages/news/news.vue +++ b/client/src/pages/news/news.vue @@ -39,7 +39,7 @@ {{ news.viewCount || 0 }} 次 - 🔥 + 热 {{ news.hotCount || 0 }} @@ -110,7 +110,8 @@ const heroBg = computed(() => { }) const heroEmoji = computed(() => { - return { product: '🚀', paper: '📄', tool: '🔧', company: '🏢' }[news.value.category || 'product'] || '📰' + const map = { product: '产品', paper: '论文', tool: '工具', company: '公司' } + return map[news.value.category || 'product'] || '资讯' }) const categoryLabel = computed(() => { diff --git a/client/src/pages/profile/profile.vue b/client/src/pages/profile/profile.vue index 4931238..7a59461 100644 --- a/client/src/pages/profile/profile.vue +++ b/client/src/pages/profile/profile.vue @@ -2,7 +2,7 @@ - + - - {{ (userStore.userInfo?.nickName || '用').charAt(0) }} - + + + + + + {{ userStore.userInfo?.nickName || '探索者' }} + {{ profileSubText }} + + + + {{ stats.streak }} 天 + + + + + + + {{ stats?.totalLit || 0 }} + 点亮星球 + + + {{ stats?.streak || 0 }} + 连续天数 + + + {{ collectedCount }} + 收藏知识 + + + + + + + + + + {{ dim.name }} + + + + {{ stats.litByDim?.[dim.id] || 0 }}/{{ byDim[dim.id] || 0 }} - {{ userStore.userInfo?.nickName || '游客' }} - - {{ subscriptionBadge }} · 已收藏 {{ collectedCount }} 篇 - @@ -26,14 +60,11 @@ {{ planTitle }} {{ planDesc }} - - - {{ planExpiryText }} - + {{ planExpiryText }} @@ -42,55 +73,52 @@ - - 🤖 AI 问答配额 + + + + AI 问答 · 今日 + - - {{ quotaUsed }} / {{ quotaTotal }} - 今日使用 - - 🎁 分享得次数 + + 剩余 {{ quotaInfo.remainingQuota }} 次 + 分享 +5 次 + - ⭐ + 我的收藏 {{ collectedCount }} - → + - 📊 + 学习进度 - → + - 🏆 + 成就 - → + - 🥇 + 排行榜 - → + - 💬 + 意见反馈 - → - - - ℹ️ - 关于 AI 维度 - → + - ⚙️ + 设置 - → + @@ -99,42 +127,98 @@ 微信一键登录 - 暂不登录,先体验 - 退出登录 + 退出登录 - 宇之然 AI 维度 v1.0.0 + 宇之然 AI 维度 v{{ versionText }} diff --git a/client/src/pages/search/search.vue b/client/src/pages/search/search.vue index 71b01ed..3627fdd 100644 --- a/client/src/pages/search/search.vue +++ b/client/src/pages/search/search.vue @@ -21,7 +21,7 @@ @confirm="doSearch" confirm-type="search" /> - 🔍 + ✕ @@ -33,7 +33,7 @@ - 🔍 + 没有找到相关知识 输入关键词搜索 AI 知识 支持按标题、摘要、标签搜索 diff --git a/client/src/pages/settings/settings.vue b/client/src/pages/settings/settings.vue index 655fcdb..5ec59eb 100644 --- a/client/src/pages/settings/settings.vue +++ b/client/src/pages/settings/settings.vue @@ -11,7 +11,7 @@ 偏好设置 - 🔔 + 铃 推送通知 接收每日 AI 趋势推送 diff --git a/client/src/pages/trend/trend.vue b/client/src/pages/trend/trend.vue index c50479c..f0e1da9 100644 --- a/client/src/pages/trend/trend.vue +++ b/client/src/pages/trend/trend.vue @@ -45,7 +45,7 @@ {{ formatDate(item.newsDate) }} - 🔥 {{ item.hotCount }} + {{ item.hotCount }} 人觉得热 👁️ {{ item.viewCount || 0 }} @@ -56,7 +56,7 @@ 加载中... - 📡 + 暂时没有快讯 暂无趋势资讯 稍后再来看最新 AI 动态 @@ -72,7 +72,7 @@ import { trendApi } from '@/utils/api' const filters = ref([ { icon: '📰', text: '全部' }, - { icon: '🚀', text: '产品' }, + { icon: 'rocket', text: '产品' }, { icon: '📄', text: '论文' }, { icon: '🔧', text: '工具' }, { icon: '🏢', text: '公司' } @@ -116,9 +116,9 @@ async function fetchTrends() { } function getTagText(item) { - if (item.hot) return '🔥 热门' - if (item.picked) return '📌 精选' - if (item.category === '产品') return '🚀 产品' + if (item.hot) return '热门' + if (item.picked) return '精选' + if (item.category === '产品') return '产品' if (item.category === '论文') return '📄 论文' if (item.category === '工具') return '🔧 工具' if (item.category === '公司') return '🏢 公司' @@ -311,7 +311,7 @@ onShareAppMessage(() => ({ .card-tag.pick { background: rgba(124, 77, 255, 0.12); - color: #b388ff; + color: var(--color-brand); } .card-tag.product { @@ -321,7 +321,7 @@ onShareAppMessage(() => ({ .card-tag.paper { background: rgba(179, 136, 255, 0.12); - color: #b388ff; + color: var(--color-brand); } .card-tag.tool { diff --git a/client/src/static/icons/arrow-gray.png b/client/src/static/icons/arrow-gray.png new file mode 100644 index 0000000000000000000000000000000000000000..2c1ddff3017c13205b6b8ec9d6fc81a5c6a97aa3 GIT binary patch literal 989 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEX7WqAsj$Z!;#Vf`y9Ua zKjwaqv`yL}&k+FP<&51nG>c(DCpVzFng zo{zu&ysng$4H1a1VOo}h#^HVh0Y zmp)2z6brE!Ogk&ecIf`9Uu$w4Q=_tHo)wSFUU1pq-ZUZEnJJHwSyqSkZslZPP;6N> zb(8#ou0)w&uJtR-l~)S~Jo|n0+N4`COczol)?e|gRGhizpK;UGzO%1brl?M?UCp$J z{lK=wJNuSinR9K{5&6rGDtrtK<(EH}3q6{axvcTr%9=!*KWqE?&oDm7Fjf+ezCKHE zCCh<>X)bzmRoNnm(CRWA$XEz=e{D z(K%A}%MLyJ!uapf>t)NGvSt72wOiIUObYU6EWBv4+VSt?59U^9ZZi8kO;=|unVlw- zbfN8j{f?@JoOK2ay@8i~m?zIHsZsm0_RO4Y$Nu70KHk4#4sRa#Fm%rh`LjvjP!~|~ zG=}^7+p=C%UOvsfAxJxQzpuW$p6Y?`^G&Br`Ngo{uED=Bb^kfuYQMvjBi`YyTUfo<_hBhB#&O+nKUOFxNuU?$s&#?4ZvePeVhqWJP zOz>qm3gj3uhDi5w?qmtbvr&5zCGZp|q{VmzC^R#_fxGv3iwn~?sq@9JdXFFZ#j@i{ zh0hv4plx7Ao+HqP$$F-8!r8(Pk{%mw{3LBKb-p$C-px5OTxafktFCL=7viwSr{RKi zvZ&kq+5YBtIV$HmL{>6ztZ+VWaxKesO?t+P&*3W8tVd2USCnpKXQ+Eqz2#wLT)tN6 zS8s6o%xB>HbJ@HRICt|DBgSjY)xTJ8%>69?A#={X>)Z!24l2%B^8G7Q z!LA1aZZB)+vaeyBJdL@6x$R}|T=pD>?Yiv-Z<8cL)4wf!v+$`vUSscrD(3``xo*FZ=wS_Px&nMp)JRCt{2+&^ejQ4|L7@4WnZr3Q+ilN21B>L?;2 zxQR-uE)GgslINJ&78jvotIjUkrfP}~1(7<~MR8CNr>b;vPz0Aa7_hM~Y2R@uL2YhO zXx@AGT+VN6&T`0?d;9fK0w|@FQc5YMlu}A5rIb=iDW#NBDi)OX0=)zQ54WQ5DuMTV zIdA6vUH@I$JH^4u0E*?-QE>DeKmprJHsNJX)vCGIY46r=UfK(o5TeP>n-2gC405#j zBnWso-Oc*X$^gLpv-?Hb*W&Qm#8uvt#!7LqG62eYH;n_vNK_9WCB9IMtPJ46oc{qH zP4pO#6-yhl39l3zD+2(4#j0N;g83u>Q^g=0Pk5zRSQ`KU96IP<17XEDjCj6SZXHd0 zsXnu^hW@Ei;XMxq0N1I8F z_?JLfGtJuEGG9{(b2Sym14- zm~rUQgbnvUTn(TYgyYPZ>@mKvSoLcOuhf%sEvQ&(p8&RA0LXO>2`ja#f0l@>xc}kW z3E2Mc>8<4+Z~ofQpnuV(Hykg?Ku!!31E7fy+$)!GCc_e z&}Pbv+V~_Ct_1M$Q#b?0Y2(nND~k*Hw}~(HpTq|ssqvVIZb8x-kBQh5AhGe7hzvk# z<1rBzfaJzwA}#>QjmJb(0MZ+eiI^7TLgO(JaRP#XhY6dHr8gdvOxg?lOQphvW;=Wd z(7~NE0@w52z{xvz=vzA5?Vi;qfSJjq#$#f2024tt>r&$}u{Hn~8;^;VEvQrq4{w4$ zm}W#+t}P6lPkyU-Sa|}r5KZ+My{CQt<%HLXm6ZW7d6D7OZv$EX+=E-Rp2kjbu`&Q; z^Vb1<@BBzWlf3MOxjFir?qdCCa zHh-4>zOKmTmi5ED1aQx*F*{dpV_xVd^+l8;``fsxF$^5oBvtMA?Vw_v$GjIZ~ZpYXVTesYZuGG ztShrUlihCL+g|xmUHa_d``;OOWd7gj&=lu>ESE`u~9s1i&JU%-8_4c_uqQd%2Pd+E#PcRVudQGG<7#J*%=4;8tu`o1TJOAT!EngZ2Ob literal 0 HcmV?d00001 diff --git a/client/src/static/icons/back-white.png b/client/src/static/icons/back-white.png new file mode 100644 index 0000000000000000000000000000000000000000..8ed9a5dbe0e04617b4f7929963ff9ff971c9ea3f GIT binary patch literal 448 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEX7WqAsj$Z!;#Vf%;CDvK?Y(2i>d>_Od+6i)madAYJXfT`96XLHP21 zGe++8MXeue&eYB~%H%6PZ22=eYO;#wBpCX|`+%*2@m=HOl6mVG#1B}<-)lHsvM!Gy zdtbHJ^IOj~WFM?&Sbgo**8{=F8!c)Xr!lXuY2Z(ocg{TXfXs~^nFC65^Di~r^mxB{ zwTi6J2aBSQ*AM7`gpQs#djH06_O$oxH-9I7tDCS_K7|mIWzO3=2l-7t&u7~_Sa<#2 z`cL~6+~n^6EAF#nTwbm9!fft(?!E%XeZ0J%n+kfTEV2?5sPW zDDj;mBMi*|DGWHkZ~ofC|M_dC4N{gZ?Iqjt7}hhhf0%RbYVXA8X|YR-OOMX}h6tBQ eORhy_&!3#~jPc8n*;{~7z~JfX=d#Wzp$P#0V!?I* literal 0 HcmV?d00001 diff --git a/client/src/static/icons/back.png b/client/src/static/icons/back.png new file mode 100644 index 0000000000000000000000000000000000000000..934d3cf9372b7d562c2bb7fff6588b552e0f618e GIT binary patch literal 874 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEX7WqAsj$Z!;#VfhDDFKRvJ z!fl=FyG0<`W6IOR(;PDYupT>B;$FJGrGfc*OaRGJ%7AE=dFtV=OX#0tqY^R95@x^{^afYg^wqnTw1nDd+vGG zA9vNO_T7|;*}nPa#fqDZg?CnM7nSEf@mG4?^L;0G^KmGC_q|u@z_By`(<)}CleLr0 zWd(xX&EE5(;X={u>9_OuFT=Y&itZH%ueTP zCl|{Gv;pO>GNuCc2g?Rb19C1irk<*uEG%eZyEgsSO1-CNT~zqK9&XoWzj5#OPTmBI zbd$|DdZy{F36W6eQ{XyVt9L`EZg!GoAp_5~drZIWb3$$8W_zr=6}d{nPbT4G@1GT6 zDLVD5ih<4--y0k_2bfB9&bY1?*z>Mt-GLK&DN8S2sI#8^b(%3dL&L(T5@2A|G9P^r zHfhg>O(Ff3pXDZWJ`St(f0}*ocEMJq>CV;CW$zd?ZhFrQ7M!9PEN@ZCFl7b*oSp?f zlD`hTWM05h`yhuU)#*L+bN+?{zYqN6IMs5C{hka%@`S(~cFuh@3ucRRy zBwtaG6uS@K7&w;j@9AC8^S<`MS91o2S!WNH3U+=@dmA$S*!veZA8mg% z?>o?m`|GVuCL2rTZJ*>W7WOJi04VhRGxK+rADi+Os!pA9isgF7z!0$dk<^0yo946b xy4Zipcz3kyx!*$Xp4_&7tE_|q7Rmi&?!DYz)ZFxQ2QafRc)I$ztaD0e0sy36iWvX^ literal 0 HcmV?d00001 diff --git a/client/src/static/icons/book-gray.png b/client/src/static/icons/book-gray.png new file mode 100644 index 0000000000000000000000000000000000000000..0638e53166871b47365fa7cd9ba66a40400980bf GIT binary patch literal 1336 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEX7WqAsj$Z!;#Vf<0ZSwqt$_05{5 zYe%~l-u9GsF?SS=o~bHUhM$(LQxXvFQV`iTm-qbq``c$5 zZ{KG6)#mvd^QXU?4nKHPS$F<>rQPlKF?0DPl6wq~ArZy)lZ*6kNY`F$Ic?sZzW9*W z0>+4_8dHX^4r~&_62TJ8ZVmA=MspHQeqVWIoBe59afka4AFtL>F>8!pe@d!_{X%rH z??0LObJqVj#Zkpt_~pdU%GJHP0rab5`4x zKd3(PyBdCd?wdDvtnBV@eEH?&ua9?Jx*tUcZVM~YyRbUX^Y7*Ad$+!ouhb4Xx~s-) z`kF6?*#mC${dzky=UGJlm3o%vcJE$#{j>W!LqjTN?(zB=C(~|e+nyJRe7RY8?SaOi z@D9-i4*ylhA{VO2R@}cR{#CyCYG}3a&pjy*|IYb!UA#ufVTx(txqi_DHhCN|OnZ6W z?Xr;ldaw6Ui3wve>($hz=w+{@8&f;h2&{h{8d1JnJRn2=$)~rila5{X?%3V&D*ix6 zhji#452utR4Bj6vy_l09qTkzP$Fhd?8YBO-lAV=DZa0%Wz9^ifdE^BYVE2F)E^(-aRQr~hD=hx~ROeWl$FIN*&{Ht-*w)4j2au%<$?zmT5O|IAZutPQS3ZqYW%>vm#lO5*$ z@UwEYcifL)obf}a;Q&2R(zc~5<}cPwefy7@Ie+!5w%xgBoGilEmfDAHGW6!Y zr1RlTta+N^;VwJV-;zgu&nx(O+G@ec)n<2}U13|`nD}$cce90WP8m7^6Oiu%CZ+T& z7q-VHX{n);qjJt%+Owu1Gnc>Ee%G%@uP3fNyhO5M>lDen<$qFTpMIR-B)uxvJe2>d z8*{vM%7S&v-hGiZO?!HL`@_no%f9O~Z9N*PZJ7RIQy=Tf>{YCHA6}H5nbUS}#eWW` z!*3>uN&Q~*Z!!1n8KM>3PEU>0F%N6*}Xi=KYBi z@*kO{eU-J_$EBYgn&}q#bb@kqP_A>KTlAH)YTfG%SN{}@=44$rjF6*2UngBgJi)H`- literal 0 HcmV?d00001 diff --git a/client/src/static/icons/book-white.png b/client/src/static/icons/book-white.png new file mode 100644 index 0000000000000000000000000000000000000000..cf63098568a66b7f555f52fa99c3697b5709a489 GIT binary patch literal 1066 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEX7WqAsj$Z!;#VfvQRyGc zZPykj!QG$szwmMt=m{u~3JCZUn7cM&R!i627pI!m>b`J`%D7y1@yhX5b)&Y~GtTXt zY<}!Hb9>`V%k+EI^Umy@-nb_r4GCA0#G- z>{!>j%a5_Wq4q$#pkZ*}ZQ}!vpYlK2CYH6EqlWzrdzt%@qhiK&v=UeO~HU-_wzhopNc)RS+!^N^c`nXA4n(Y zZ!lN#yME*F=H z3nu%&&NmO6aIOEleKA+mS{Vl;_6cVgRf-u-(vG?5Rv4@AfAigum$DDk#+ID1VyU_P zxGysKKr@gti^H+*mOS9)|PN!UAl?eYfkhT~!x8!j$e zWxIlR!SYIQGI=prnlbAFpasavZQz0)c$GK^G+Y=KX9?*$I_&|JNqL2r-aSOwP2{Z=X89{ zLZM?41}r`{e;TKL8Y&&yP@#V5Nk0V4?)=oE3e0fPpx_;%B ztugas->(Y&)m(RV!_Rn+TN7gM|8HCJcG5CcU$0y9V*6(zfwckkOzANmSF_SD?f_IBMitTmQE?znt5AcJZri z&t>X^_pLu&Jn#AMn)&yi&uK5#tBWrJ_b{moJpVP;F3||HQ$5w(EV}=&0@G_w7!rYEIK=I2WfI zZfM_><2uP|d-8{+ktJ(3>V0*MpYMC;)s1^`rcb+;>MGsS`WR&T`%uZkjoTh}{;T_4 zy-;a@*x^85F1PTCuNp0RH3&d|9t?<8ysIB~3tKfLxM zV~X~tr-!bosAdUo`w(&H^K;(|hefNbjCUU1npXWtBuww+-yJEj<-4YzYztGjw9RBN zFP40)bIG#%jgW!b&4`v`I+rZEqExecJEIRoO`O(zt#R$#*Q_n2(_PG6#XhK9joe^7 z<1edJZ^yASCS6AtcdfNN7Io$d(-x0i{5Pc%bT^dT(=L=g$#h{&`M--354M$yMQu_J zuDZ17lhx5Z95!=T?EgQPVc)hDX-_UYue{8yduis$dYy{ZPPUGh7*Zs5{WEQDQ!(x5 zS?0&MykYO&y}Rlch1^&)W9{U`D`z(PNdCB})OXGOt2;Z}o1jVamYrBJzjD9%Pl0oh zTjd-@FJ4do&%oZm9J{y5LQl-z;nhCZ`{6unYxn&-ywL9VW4`UX6}qh&UaE<2S)QGq z=U;QQO|aju{{Dk<@65{q^^boR#GZ>^G=Cx6M;-lhe6**RADfc)T3H2F$VL9WDz|N6 ztC*R&Eb-yjb-rh+9=_5z)Ad8{huO;`Yfi=oX1CmGoSMeNBbz1Rcj?BWc{!VIsHKG- zUc{HMtGYDlTCDJaDWJSl*lD*ReSP&|iN36@uk&WlD80^Vu=ICtl3G~hbQhl^jmqs( m?9ShxAeS}3Qb%mze+K`DTD)u$W=nvjA%mx@pUXO@geCx9JbH5g literal 0 HcmV?d00001 diff --git a/client/src/static/icons/bulb-gray.png b/client/src/static/icons/bulb-gray.png new file mode 100644 index 0000000000000000000000000000000000000000..51882f0535695e797b17a1d2a7dba6745b125f1a GIT binary patch literal 1477 zcmV;$1v>hPP)Px)Z%IT!RCt{2+*@oEWfTYS|C!mYrNv$ViHSaFEgE8s zF(n2fiIHI+m|#29ZFtxrCf z7%x#FXrW*W0=2t4#|PL1XJ%{LnfYc?&hPP?+3%dpzdN0LGdq9~LI@#*5JCtcgb+dq zA?7;ryMdW%O{P|WvbVs{DgdJr&=vuFnt@XU`ja*O`YLEg>gywa``znw1U`V4&eYv7 zva0~P3ygcG|4IG^puM5;lHSJ0A3Eb@k4^+OfR<$cMlin!V5!5YCIIXUQ+QiEzV4Ju z?YSm!0kkChVqok5aGlFjp9Eo3A|4xZd0s~X6+mmUuay~1Ue+L@p5}P8#>*Nr5r_b~ zx&{|dr2l!BiB?Wy_X`9J1Nei9MwoemfZG660$4K5l3y0;^VihXJ@S{s)*bUbfMjxT z;UrHU0dVsayB#IaZqoSErux|MRP(iV^erW(wFdLc0Osf2k?=zp1*_xn^}jjk-zi@M z*t2Ko>hbdzJ_m4no?S+u;kDvWWp{1uvh+;mP9_HzP9psZn705Z$g|t;n5W&5cqVds z=H0u)w*XpFsd*U5ehknG`#rcfR9;%|^b&G+ci&B!N!kVQ!+CZ&oM?#M@1%wk{sqw9 z*8|`wn@wjx+LMWR!*3rKi7cU$CgP1y+8Y2B(*H1LWIsw@Ir7t+2KDW{9`esT4U}j?G zZPpDKEh#R1NmUkiNe$$p1b%BRYz$6tz82v0lMC@f$&02&7)^k^>JxUsLf_m zMO(*#<*NFC@>5XOc+5J;Cz|8Yx0Ut*pg63(4&a=%9ezM%AIbtqrBXV;QOgFRy+p*S z`pBuRjf?|Gna7#tXaL7YwOav{TN=i+jpo~RS)V;L0RJlo6+&6 zy6W$g^!e;nqRrT%E)~*=^@V$AQn5`EI9#Izn=tj#R z(J!j%PvMwlglUng;_6O8%!TG5Vm_~|etx6o0Vd4B0Ep~vev~MyA5h8y&;UyS4Dvky z0$pP>d{R|?WdRsqn{R-T|BjU+OuWE6rV-D*0CbS`pB+f~VxR?Em!@IRud2!`I{;hb z(GLhXZW``XRX-mhylEOb+SCv|q^f>MRUrT%J@gZ)gT-@xtpyx{*@%w@8fY|Ui{3joSsWM=)al-u)pAy5H; zVOg03Dm`Tco<9wkrTL6P)_b%uHo?aS{l)@LOuNcmM!Z}xYwO~ z0vW)?cl_X60A(&sIi?qc?%cY$`bU@Mb4{SGf^srx+T`NF2Y|9nI=#Wg`CJpo0PKFy z=M<6V`kZbEWB@wxLDwhMf_43$6UhGo0MOFezlyWQbY5fmzvTK~2qAPx&tVu*cRCt{2+)s#BRT#(d-${eAOld4!1eGPxA~zK% z^hcysfCd`k$-0#k_?HyF1iE5 z+c~dy=Dqiv^S<-@?B={P=XuU~?!5QjbN@(YW@ct)W@ct)W@a|&Lg@q+3)mv*5lI^) zJuGRRq!~$(?Y5m-L6E^OF9S z^jNb)-q?BfhSw^_bBi-@aIkX{291E&nPPetOkA@ zGD+uvw}4xh(DxMZ5paD-|EGbw`x#y3fZKtiLni0*E)F4qhk&n!#QCN|dM z!0yBZe4H_d1Bo1V=gOgoX6rv28TdtDb;dlVnk9-sr+_NXjd}q*o!IhTFObW7iCnhi z&LtaQe`LVtOKf$(8sKUqpLcTSk_E5=n28Md{en5g4uy}D%BNSrro_Merh+-W7Rl#d z;Er55+>$Ghqz58xb0LHi8FLsEI|~kyw&hA7X8|@u+D>JR|34vwOOj4TKDsYg4mk_( zV5IHWjPX;ZBW>$)1$BxNZZ9+!Alq5&W!O> zVi#m4vH-E2UC#I^DV z89xPhUeabsbCM2+5PmA1lbM;BnOTpjy91!PQ~IUm$Ack+k(LFz?r)+28-Xhe&Pr#2 z*oB-T{q>=SwU~w8XwH)7t(CN^qW(HpTLEG#lj|$$uXD8(VA*j23UdJpa{&r-0Sa>g z3UdJpa{&rlSvr9h9tZX<^=~6>jF?ZS#-ad;iCn6x79jlg!3x?`ydk%*<5dSJ1o#*K$XFUTa#?7a@cT9i=w;ns@xn zCLU{DidmrT>&&vM?I>v8o6YsxuB?@`v%~&6Q)>ZY5A@a8=g4X;z?VJrS=I6Xyqb5y w&$WDwWg9;JH#0LcGcz+YGcz+YGqdIB2B_Xc!L!EF`~Uy|07*qoM6N<$f_>=SCIA2c literal 0 HcmV?d00001 diff --git a/client/src/static/icons/bulb.png b/client/src/static/icons/bulb.png new file mode 100644 index 0000000000000000000000000000000000000000..7d43858a98c12e200bf60bcb2e256b31fa56acc3 GIT binary patch literal 1454 zcmV;f1yTBmP)Px)SV=@dRCt{2+)Zp0br=Tl=bhbUhL$e@iHRN*K|_o& z24f(S_z{#CBNq=E+wE?P5DbZt1T_+)96Wd+@hcQv?RK~A9wbqN8jXrEeqj_(9!!iM zQ6VZ&5U|o+b~}%kmdMU5ww;;(Ov?K^zBB#5Z|7-ur!&(9lu}A5rIb=iDW#NBN+~td zN!$ZWRaZ}aHQmP51eStW29d=Cj{|Z{Ab*nlYJR)DzqeOL;+}Of5+6XeyMD9lHr4`i z6M$PM|CRh2B4y0Ahbk}1sVHX*BC!Epn(cMZl%10t{Gl?Ck@)B2uiB>kH=gNi+Xt}R^Tj@0%O zxT^JDJSdjDKw6(vO0y1}Z@ugTwH0h&J;^5k%xSr!;Cm-!uj}iRUjuyu0mT|XPY>tT z+}dX#uJheNcvwW9Zp+v&7Yb6J%GzC9xM*yo_9TD@0c8C5;%`pcxxTMYj!)|w2tjNC zY~9M)M~+uN0%W!S9vs3vXG_pi$Xu>^WdnE~z+EkN*;gq$w+HDFL~H?c=4wTP-9DR6 zAtm`r(S9@3e)-WIq&(%fO@N(4W#^GF`%P9X0c3m5+~AJ6ey`>AbUL-R+?V+@ioTt> zS_i>0fK>A~OXQxxlKn;$eb2`--H@cXV{XxZ8p%d=@Bv_`Y*!$7$ZwlV_mxeX*cMgq zR*VOb&DS=MpEfqzRq3~1i@JZMsQWiF8URTlxZP*Vu${6WH#JA9 z-QG0*_rtkU2)2|qMTz0GwIDT@eA%`u@+kdp%0 z*+t_Yy1Vh|9FZUD$CkI}&zcn>Ov$o7E zPXGV}!?oK?&0s753YPkme=^nIPoW^g5Pa8MC*e6!h>E#4Vd4s(+Rw)Nw?Z89)I=7W zYHut60UU3Qr%m;5NjThGB02K{fWYVGhs-n9KcNi%9pH2KNf3Jg0=c-kW?6F7RDXl- z0rUhwvh^J+Lr5(2)WDe+fQa|aPGYzi=)-eqT7X|nH8*wu220Mn0Do^P_nPY8iU5Uk zD?B*Z@4RoSf7(X{Boc;d!Nbveum1yXB!V@AOJo2f-wu0CkyN72|0g8! zJpcf@da7$(my`Jz%m1b4|4J#Plu}A5rIb=iDW#NBN~sIfzxT_k5dU-SkpKVy07*qo IM6N<$g8$Bz;{X5v literal 0 HcmV?d00001 diff --git a/client/src/static/icons/chat-gray.png b/client/src/static/icons/chat-gray.png new file mode 100644 index 0000000000000000000000000000000000000000..03bda6adbe1e9ff0e90f131f55afcfad300e98e8 GIT binary patch literal 1213 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEX7WqAsj$Z!;#Vf_U?%a82 z?Q3ULW_RX)_uY2?d|}$X_qNYYKlz)G&Lkj+4kiXBt-5gJ$n&?SpH6*lbG6;)YLAk8 zN?gNfxkup_-^HH%@=fpW{3I3Ib>x}+k#+nXx4%O7xG-EZA{?%c%2d-3K`$Ir+W954EK zEts)g|LX0t>&>>d%Oa?>Kf$w(F zK&y0LRr7}|Fh3MxAl-24{6E%*oItC(f5l%YEztB~-Mn7S%wivd$cr;im)|$G^cP9_ zd^ysg?3Z7~IsL>}M||~GEiJr#)ZY7_|1yzZZf5V_g+GvX{}7zBySVB{!20*PufF>D z&CA>!8dG)ca?$dWrtMEVwyb_6>A<|dqtn!Th5CUPuV3!?d+Qq~gMRpq9J!4@CeJ)< z8*Ub4{C(f;wQ_dGn-9GbJR$xv+4gLqWzpfKrusCS##XZ*Ry6%W&OyA#r6*{x$ z-wck@oXfo7T6mlI7dv&sKVI+8O6jj%r6lOE?SWjwv=|28MsW_m?PldFzklzZmc4I> z)@C_}br1ShB$cf7b7wj3-uET--5br)#on{Kka`Y z`}J2trb;{8E9I-Eac^1}7CT|hyQXV}!X4_DR?g$T8XIg9ai96!xBS^RgUxf(R0Fvd z=-oc!wMfuGcKgh!5&R480eM-M+SYxH-gu82sKAtSf#Z{82JUj%C?SvXDhPE4Oa<)FWwbX+9;I^G!N9z^-tqiQsY;f4i4_VzirB;(Td$GtW8E1B-l0#a4Yikm>3Ditmi*ftx_? zCes7UOWB{sv}|XZ-O%3{F1$R3;T-e+=eyDrlMk>Z7=65%T|WKIyXU|TUW@As^?dso!zAMd`^{rRiu z@_OOp|HSt#;e60;S!QK?;MzU^8v!rQybtr7ym!0CYPJ7L9}XJ$+Qo7@T=IJmb2PlV zAv2-a#U^9t)?=dl4wpmTfPbhr0@Q{VEg5rHxlgU2G;Oa9Q4UwZY(s}|2UJxsoXUCSk8ou z?KA(DC^js-m-g-QHKjSf;x{%NzL&Pm;nJ>q_fOCNogA`l=3K>>vYzUyvXi&$DzQ&P d0Oco|JfDBvy7k&*D=?!mc)I$ztaD0e0s!VBr>6h_ literal 0 HcmV?d00001 diff --git a/client/src/static/icons/chat.png b/client/src/static/icons/chat.png index 251a83ec24a8ef1ecd86ed10f1b30d15fbbbdb33..8256c06eb13b363c558c2475c39f6149cf72887f 100644 GIT binary patch literal 1179 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEX7WqAsj$Z!;#Vf3gWI`^n;UcEzoLavRyRcu%^8JRi}IfZ`))>h66`h_|_fyIRB*Mk{I5* zvrcZz(f++UnQP|FTX6+Xf`?7c**n$?$sZuXg;zOnG5lCI2+d1Pg{rg_~NZfkL8=pX!$VN|+|(#=PK-L_;di z0VB2qN#+ebj1k8ebdnjS88VzER@hj&VVTW$%^lZYNLVU$9VuSGwRh^$wu=lbN8^Qi zyO#U(O;p#KRpGE*VaF_xFc+T?qlQ7qx5ec?eUsT!?W_2;*tcynzw%SpTlV;qB{Fx* zm-bm)TzBC0QUxo%2hoCmE9LiaPwi5fw%1e`gMC{IMf5N-i$ijxlO22AXKX!TY zuWYNA;}=Ge%RBGuan}_nxIed>XuqlK?Fo6YwtI(Sj#{fVX)0|__pG?Pk-;q3QF&vGW_rVWcju38e|QyETFWwubsg}K_@-Zy zvnh7k?CjSsR&G5keW9DnB(i4j8>@E(5>>XFf1L1U(C;~_F7@?TdBc+Zk2H5Y==gr- z=jTl~zMd?N2-kC5JEbXO)x8UwefRS;eP1p5ee3^snzqZe(;G5=@os+OF2D6w8rO%S z+-uca!^P#z&NHy8Mb+vloexjH`}5t|6McDan~ZX#N;d}R^qo2TCHCE$HJ=p4%^zB` zL=-3-u1@_paXCX(>Fk?+c1$lG=B8O++rDt`zOy;Yt$>eKSK| zepZy|#K~7L{N5q``1#C_@keHT{QG00l~g*z&Ziyo=FWZV|7X3d5(2#HsAt%eWaaE% T;(Z%fx-fXU`njxgN@xNA@(L58 literal 297 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCwj^(N7l!{JxM1({$v}~No-U3d z8I5nRTk|yq2(&)D@BTt3Z+W?HUUN`v+g!0@QzJI9UUyR0opOYYd#Pd(bD)Ipx$B4K zoaE$XR#KX#mdpRhqOWU_MQG?&#xU8>Mh_m%u~zXck~dm!mOcB+z4?u{8(!?_xxV}L zCY{A5zU!TzU0v4rFt^{;cemiDhJwa*^MB5L%2DaC<3M}>W42a;FYB6>4O;^m-!$L3 z4Hkad-1e7( l%VzI5z9uh8aN_;X3^xR8Ch{gti39qS!PC{xWt~$(69CIUePx(L`g(JRCt{2+D~X4brc8i?{9WCO^rkkE=9432c<>? z5xt1k20WBjV`{@Dh4hkxQ8yd2OFT+2HxJFf-35aXq~x$!QjJQ3f)Elxkcx_jQtiP* zp;cF*hNek(XXkm)mXQ3`w&VO}Cf)DnI1l!{owvXF{g}4^5fKp)5fKp)5fKp)5fKp) z5fKp)5&1%eT|m@~=ZjB4WLT6{j!W}<>GUfXP3B_*Hkl}to^xIIt4)_yZQDLD_(s?F zrgB=QK6s}o6c+ZnHTUB!ms_2hlQEf>4FE7b{n=gD+>O&vxNlof)P~92Yyg0@%GFr_ zPv~M+Or0>9yWtx!o|_v5%vF~l|4C3aa5gRZ){cXH< z#XSwE`!(U!CWV88y@ytVs>cp0QBw5XIh#b=?kZ_ z257+4>|$$0@Hv3i?NPZDyW2Y4-~V(qjM{LgH$X%z=kn?TfQL2Vb!Ej44fZ9=A(VDQ zEX?Gl-IHjWfS^YgvtV^)29nFcSGBc{JJdvOt`{PM+MvSdP&)Z(;6-T|KLt&FaO%O@ zHRoLbu%-DO(-S7>2P>O>luh>>HC@B4GP(jzp8TSvwo&~?MEXReuZs2W#`5!zo6MQd zpL$3$Sow#-k1b6tp&U2sG1`DX|GGRZzyq5u?SVK;x!n9d6ZytUrQH>&E@}rWKzWyH zeyP9z<*O!gdlkkWkf_30@w*8kqSS9v1kk1lS1k*NQ>o4ie&+Op(FQbG>I8saHK9Zm z&dPY9_?Tb0#`47xfpq9%%97WHQaxw<%H=1c4M?SuSK_eS0qD-!330LP$4Z#Ul{ysE zNS*LxCUxw?|C_^{$G~sd__WpFb>ztR*j>=%gt3dXEbh_(Y9QOWt zE5YlY5T(J2SAlK9em5a%gB7m=Jq7vMgtf}~8BwS|SQ+REbg<%8;Kxuun($wP6{YZY z7=snBLL10<-I${~Sn(>fmvLTJLX-z9UWNKL^mdvM^}&i)k(deJ>~UnrtpkqZ<{@a; z#MhN&J)21-e+Zyd-$kMU05J7_sm-al%XMC))u*givja)}+s_>!G6R{x*OO=C%6`7i z<48|obSK|@L}>+FcN;4qQiBz*BC`UnyTz4o7JIBZf3VtFBG&-i{B%^_Wi=nnCX?$S zl;*ogHUR%NL0NbKkl$}y6#Su}jzn>=;#K7KfUOFJg}oKo7!^@f)nt!k`j1`;yCxzc sA|fIpA|fIpA|fIpA|fIpBBF@;8>!i{Zi{>JUH||907*qoM6N<$f~grRB>(^b literal 0 HcmV?d00001 diff --git a/client/src/static/icons/check-green.png b/client/src/static/icons/check-green.png new file mode 100644 index 0000000000000000000000000000000000000000..eb0e2121ebf9b3e56c2ed71710af1e0220001217 GIT binary patch literal 1153 zcmV-{1b+L8P)Px(D@jB_RCt{2+Cgj-WfTYS|M$&yY0*H4Hbf&KvP)Ai zDrrH>RuL}7s3D>rIM8jklHN2z>d}C`c_7i~i9@?=IB-%l!IMb?rrV_;f(LBZEgVS1 zbTvvtXk~Zic`&Qk&YI0Q-^`T#|8AX^_Px&Q+i!PX0U{zIA|fIpA|fIpA|fIpA|fIp zA|mpI3cG-)Nls?E#Ic6~RgSe;AI@}*{$w&A8*q%GI!HAAdydxh{8U%m2MHrH01;&FJ;=DJN6YQJhCh(xUOSsx0QN6nGd3trkjB7i$g8W zf^)|0<||-6-h!8cy7>SQ3o21m{PxGf4nt|={;iO>pFjQC$IJ9`a&qHIVPacYBJt%G6Ig=Uz5 zWZ~^KPW9GhC_KI_D7dLtTThp|#>$}-cfU`-Q8}u`zIEF5<^v0s(l3X3^8vsLGr4Iu zUflSR0Q=oyTB|opsS{^|uWCshVHaRZaw2^|An&-vv{ECbohQBwzN+Pnp8?6j!N(l? z?gs$S(iVH)^o9v8ip1LypfY_Du00sdJRu=3_$*KM)Oy=A& z@z%zri!lOr!A#-DPY+L9zR}=qOw@=DzHZpCMF?DixsZ|JB0n7gs3f6 zybAPPke^NXYq27cS*&;!_}frFn(&{C72v%v7Aszb_Bh(>nGn^*idUgMjPo)HQC_Tg z75YM^(MgE**!HIa>~Z<_1q(X$V{i6yFxC1i5)A-=WFfOvs`lAB z|Djx$Qh0qfm2-dkxdKFHAyeuZyJ~6uyxW;jk5+1ACEt8RX##2*o`i@kR=kSL1k^M# z2};z}^?1njbHGXyxe2JLcQ7lUj25dqiqH1Fh};C!6jRx0t>Z5Mn5$h=f?uH3zWe3o zBgzv{lbFhEK*bpWq!M4%dgO=g$8UyR6A=*+5fKp)5fKp)5fKp)5fKp)QAGU-Sjos& TRI`eH00000NkvXXu0mjfH1ZMB literal 0 HcmV?d00001 diff --git a/client/src/static/icons/check-white.png b/client/src/static/icons/check-white.png new file mode 100644 index 0000000000000000000000000000000000000000..4a11af48d22a45c5873b2e21a14f38c1469ea75e GIT binary patch literal 660 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEX7WqAsj$Z!;#VfiW<4;hzNe1$I9m zQ!>J5u3_{6n}U$QKg9`_rN`DVvWp)5&zrLhs6S!*vt_?6O82c{oP9Jh?wJCJb^7Sn zy=Mds=36CiYdHGo^YL}NfSuW0mIdNY6~ayt+CP7Vb39C3fBpBRC$ob2cd*s0wp0$6j(MGV zwzPiv%F;Bx4M8HiS8)ozXJOZNK9bYaJXP>$29Pu~y$KYVekDX$W{pq6B{d$&HE*_+ zs~Er8=!C4!gC8%y%bRsn0@U@NCTw7`9GN+mEc*1TO9rJr} zE@5e|@|%|A1jW*GYp$9kYO!laGKDqDGat2Qez*MGRn4RGpMPrzKhS%n5;Zy}ExREv Y&D>&{?-#cbn5-B)UHx3vIVCg!0DtcxoB#j- literal 0 HcmV?d00001 diff --git a/client/src/static/icons/check.png b/client/src/static/icons/check.png new file mode 100644 index 0000000000000000000000000000000000000000..180d36fdd11e03f02deb354ae34d677919b66fc5 GIT binary patch literal 1099 zcmV-R1ho5!P)Px&^+`lQRCt{2+D~X4brc8i@0-o;Y#Im%Mo}y(h}4Q$ z6fdH+y@^V(dJ(eQ?51^ZiYf6Zt+{z9B6^CrI}N+glUAXggkU{ML92%n_0WT0r71lu z)XZ!$^E`wlwfSvX#`*J0_WL=`gS;QVw{L&5zZZarh=_=Yh=_=Yh=_=Yh=_=Yh=_=Y z5+Qjnz&4e7?-@klyh2vpg8NakDSxGzj|I3*b$0!QC=7qN?NPTd<{Vo3qV#iGIi0lr z@J>^&>)t5b`09?wlVKRnrJ0un0MKaYo?aAO0Oa27QSsBv%>n?p6$EVnPnlxk;7ppi zN4@}++S&quqsE{k)poP|TblVt`Vg+n_(v4@+7uJPCvC6%UfOFI*$$}HdIuCZHz2(H z*dy-AEY>lS9Z;|9R4?*>0ptNg_$nsd1D#K$o5fm2t^*nkE%qY+!hp^17D{+ItIfwK z7T{LkHvxIh5Q+)~eSPWk^6y!$XQT>Hsre@XshDC8Fu&9sU(R|>BXb5En_W8`hVq^H zL#OS|+3{)~>l%p;IQF*xco@q00h@nm!Y$9{u`Y~42TZ@CljsJQ0X(`rs@F7)iH4ndY-%Te{$F`Scxn3uOQEJSH{FSD9~*`c#`-!vSqLmS7uic zX>@{3gO#{4JA{+&1ltBHab@}zlw=WX8?3~Y`D1)n1fqd9R6xh=_=Yh=_=Yh=_=Yh=_=Yh=|Bi{{l1l%8otA R@GAfS002ovPDHLkV1l&X7ajlr literal 0 HcmV?d00001 diff --git a/client/src/static/icons/clock-gray.png b/client/src/static/icons/clock-gray.png new file mode 100644 index 0000000000000000000000000000000000000000..5caa9c37dd7475f68b637700970716c410d11f4b GIT binary patch literal 2469 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 literal 0 HcmV?d00001 diff --git a/client/src/static/icons/feedback-gray.png b/client/src/static/icons/feedback-gray.png new file mode 100644 index 0000000000000000000000000000000000000000..ca028558f2d66b8e0abbe7829158d6d0d3b03a60 GIT binary patch literal 1298 zcmZ8heKgYx82(LbLmv{MjZu#69ts_|<(khKGc!6H>UJw?mk`w{pVu&7MIC*Zu8oL= zd_-h739IXyyM|hfWm~wH87>wxYTey`z2|wK=RD{AhacQMjL$} zR{K!acItglgN5oL>dox@+Mlsb71bW}x*btKSHPC61oL-la8r+a1P=S(LT_!JGDdbV zfLfBk1_fS_M-aJf!z+W`UULHjy}plVK&Z4N7Mbv}&PFN3_QG}Oxn~XH28CztUE|1w zO_d%{m=67PN+jE#mHg`3=gnjf=)qsIKCnJn&_8kw?EST3u?z${&bHXF4u(vHyro#Z zeqK3||5v6a+&y5(_&C*odMc&ozt(J{nSrz_s1%88cEuhSzxa)1PfH~ zNC9p!M^S%ycdCUC$P$#3%>a&UL$lh5!3r8H=dJ<6;`YVe`m*b=H>ZQ$K(gp7#7pRe zO6lj=Kf7f7Xvx2_C2rwhSi8=sR{yNRmYEEyEqQ+yTCX>&P$jvsUiKAVJW8j`-VD~_ zbV@vJZl{bOuHHV<`TNhiLc1njO8~@*a@nWJZep6$WaRFQP`dN?0-ltjTV;V%jbpot z4eRce*Nk>QJJy2uIJdXShRQ$S6E~ zYdxJjFa~dS;9!dQDm!ohv)U|XK~h#{Z&H!Ls?Q}B*Z~lPjQ6GEDm&G%(jg_19;|ZW zY+Cu4Sw25pOC=7e|5l}0_efhb4-AKJgR{&_+63-fa2<*5Jo&d|=u)d|Dz$DjvA_Fx z5j!V})ub=Q6W1^al(GQ&fCNedOW!5!g=bH@vL;mXJwIIz+mh#YJU7wi482EdrHDsf z#t`ox>n+WQsx4g9x)xd5wUoVn{;{0?4SM%k`ug2M(aesGw!w2REaxn!plFGDiV5!u z2PE&}OhL+_(L(B|Eh?wL3L*@ygW%KcU z@$s{xP3x<`l0y=y+;V~N2U2NEOU&`D--Vl#MUc<|7+X<@UTMr~QdW92bL=2Ez2U(V zesgdk>FY_%Oj`eaNaC4YiZ8=TbH^9n$>Np#7PH>Q>m_+@3CPsFN~gu9O>4HJYEv|h z3Yi>X=oN$dXN;O>y>4o%tv=rFAv~1Lb|^Ocu+RIKQR@w%1Ol~U6p}* zWk{U;&4<&@BJm?fgC*4yI{^@}$)7&ly69&R1^7SQIz+Ge4%*5zbw)ALh}Bm;L1k@Q w%}`V#&CY>D=1qPW;jH-zXAQUY#Z*-qXx0;AF|OOwqA4|i;N_2Nz=mf02YPmKEC2ui literal 0 HcmV?d00001 diff --git a/client/src/static/icons/feedback-white.png b/client/src/static/icons/feedback-white.png new file mode 100644 index 0000000000000000000000000000000000000000..b840f6ffb3b2d769f93bba08bc69cb0513313a71 GIT binary patch literal 984 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEX7WqAsj$Z!;#Vf;yw$ZOobsk)+%rp-A4F5Dl}AM&m{J@d_-o9XX%KK@&q z&~2MOFa5mj`Ezruzu#j{LkBOFv?nfUKERovsGaT~%eaqSk4aqV(f7M5-kJ?d4j-^g zhc}pDE@RWRFjGORf>k{^5oaeL-+*@Mv`BmHlUC--VdCzDau=cEc zc2n`e(ru12yFXMuC|LMf=7XZasbA4kWf|lc<@CPfx*hSI+wXCSajNazp9xkkPG*2i`i)!eU1ZHUSEt)f{zzcwbLdV_D%8m2o@8M`M<*?r#Y z^{uzLLMfsLmOWt#d4`I@{%S2-VtZiXv+L`X9<8tJy1#bSdj?N`CVS@j=JwN7Yz_54 z?p1sl@^IFPld?(Qyre`QxlJmoNx#-N|J#o3tAFxEFnklg_1(WBerH(?m;RxtS9K2f zA8=b(yZGhn>YA=B){YMQ#`MOmx@WTwcwN5!`iD-azq9hAT+h{)=doSuJ0I}3+M)PB z{F&@Od6k0e&akdPLprM&mOf9;^Eq`led%)t@9E~pPH7!5Z{5~Dspxy;OUApObA`@q zV)*V+rK?&0-lIx($Lb4TV`tk$Mo-COsFS?>SM!732gwamg0FRp>{##2H9Y3R-k5yg z@8e(Uw#Pyy`H8>d%iXT|@&e1wX)3X^6yoo%DKozP)N^;-rB+V1pQrdL#Z=dp8${l$ z%Ps5LHmOWXN^n18KcoDc50|2km3l-m-ODapcxG~c!_$r(cgzgFRy(--5fD6m;PV0b z+$7bz>~k3N&dCI`$MtnA7X0|sw^jV1F5B0Qn?vQf3~roU z&dw_FYs4QGPPbzFKfnI!n){5|t!Dmp;VQZwyY@Vt^)ssY>g8QZ rFZn$4&3>=UTWu?6j0B`->}UMoa$-keTe2%KYchDc`njxgN@xNA+sMo9 literal 0 HcmV?d00001 diff --git a/client/src/static/icons/feedback.png b/client/src/static/icons/feedback.png new file mode 100644 index 0000000000000000000000000000000000000000..7e3b1de77e2242c24c6b049286d0694976aae5e3 GIT binary patch literal 1271 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEX7WqAsj$Z!;#Vf>u9Go?EtzeetxQP6rJp%}XZ#dd&pC90|0ncysOL zI;R~1!jFAya!xDb@_cz(krr(~76H{YH6e8Isx z{kG9t>H5v@e#_MFugq89pPyED`=;iCDM4PEOVJ67Nm(WZQ>-mN%x6xX9dh0z=|WDM zfuo}Dw(Z;FtV+9{nO*W%X*lRuf8M5mt!KCDFHaKArgE;l|`QM|4HF8tf;(xO_o^Kj!!CdgZ*CH`MOD z|M{5Vn@jo+9*6rvAGcgdpLxVbmZffzWB>J;`{ML=zAgOK$x`ryqx=p>)9Q=QrpRhr zrgon9n7zh)dU8PF&mB)KtlipQT(^F!T*N(VHH*KFymG>y|GR!L7=&d>vl(oynbZ8> zp+&6=Q$j)f+>7!mRm<;upD4J<{gH_PSHm_J_spR}Dg*bvKon4jU*9?N43bQM1rEIg1MH*?wRr4rl;S*Px^U0C*lCpT(_cllnC zyDhxNVh-Wce_J0gz9=~_5-iwU%@8_qBcJe(Q)dmC;x~2_wK9cXj?+K-LU%<>iQTQe zm+xlXypnzM%I@mryGu)s{M{1xN_EMD_Y7Q&f3B?fD!p{}hD50bbY#R>F!#G1=l8BJ zeuhad_n4pV>NgKl`&?#XWbe)<+3)qm&seQ*&w^~hV~@WGa30ts$l1^s8+pDycu~Dn zs_p+=gSQcOa+_)u{p2UQ3z}`8vtF^I$M{3m{{{5|2d2CHN#?%O-`=kINvzcNr_Fs+ z`CsRaFQ^vwCC#vsym8(kfA7n%(|x6^9e?W`u6@3r{@|!7-|SMOiZu}>`fJN3JZH5H zZJEz3^4j*a$d4s4Mf^uK1%8XU%$_@)(Pv%Z4O_?Fy;u28Jp21j>(FK19Y6j@Ki&I1L(`pvfrT}tkI70(m@vfx-e;}ylyxc{7o%FR~fe~JC2owxGE;}=0E z_i`-Iw|=qzP|swBJJxGuD>N%Mn90{2TyS>M^C>n~adL%zYj#I{OUVn*k5@QUovi%s z;?iWGfmb$Mdi0q`@Sf)qq)t-?$cJFr3+w zzjXa9mVI2OtCmHI$#>=-DrH{AeDO3BLxq&2Re+wA@8%x{dpY&9*4;iAq{*?MQe6HS zbH=-)?Gy7Iw}0^2oV(+ziWI|TuYRkFSw>$%DsNa$fAZYCDP?_wYQCKQC7`C0ag5un zT^<~bKhW`iOVpa>r60vrf0>9d6j|Pjdw7bo;rYs|mUdgTe~DWM4fC{|Gn literal 0 HcmV?d00001 diff --git a/client/src/static/icons/flame-gray.png b/client/src/static/icons/flame-gray.png new file mode 100644 index 0000000000000000000000000000000000000000..0414fc3972553e425e9455e60e9d1375e7eac16a GIT binary patch literal 2877 zcmZ`*byU+~7ygYQ0uzRGcMGFIDT$5v=#qvBNJt|f?U0aAK#-IWl#ozDCLl2shlEmt zjS(WL17w1Pl;8aS`<`>&`@Hv`d+vGP=iK|GTbdg(&~wuR0Ki~u1h=Lz^53DQp~U5) zyH_bheMisOmX?AzTCX%p4!UD)s0Y0I!utaN=r@evy0%fJ+oiD~w)<>@eH(9|azLyy z#BXp7X4LH`W=}YkF%ILbAIb5@up|=|(-ZU;~_|jY@~S~)A{<6x(ATkP(brT4OkCp(DRlQ ze665B+0cdR{RrN4h7qd@zPh+d`~ib?)CK*TZ@QLSz$}cX%B}2)Pcqct+|0eC4LtD& zw{Xl6%LNL-eo}YfV$WiOEm$#~p*-?6@hVYEQqG9#q04zF%NHwF06uFvE77sv==aT7 z+v>5)p-@@_+*6RqOA%*Og5^#_oMLRfqW+?UAnt|Ra6mWv;MpkIQPX}symJ6yQYaaG z%l>A}Y6O*&bA{|Co@NnLXS$p)rB(e~gLr;MrSS+4lm-XsUDbxX_o;TcX&-e@+kEfrdR65i_)PN5*1-})jx@Ql1ShG@)Yp?*jUy*l1F6v4O9sH# zf{tV3@lg_KvkS^~I(_GDUWgs=T}{m!d5m_&2DielB6bx){b?~uEU~w=nH`o6Z=B5q z9LDC~%fV31s*ct{RSY-0^!>Y5T`3QVYXdw}Mh7t>3Bl8CztDWURz_V-i4xp}5$2y? z%$Vlb(>b)0raff68!uW099(XevUHBdJH(s9YAcwUjhDG=2j8^U*Ry+@hR}R!p#Ht} zm5nB(WMKbVVcIO*>*4f+DoJ(9l{!u;w4x`la3Y7Ry;vxiUq8ugf(PRu5y}88FQY7? zC5q4W%qAekzc_#k>=NnM5}j@Zs>diH+)6=}Gt;#g3`>~Yw1mbHu=Ki#D3g{T6Tyw$ z`KJPNCtvnYnbggxwYo)up%EA2@<*(<-}a&vFDfbQXcYFBw0#{$4q_AW^mnpF!EAKBUNe( zPL<&=W0`^o(`T`013ixvEOj{Q2!YXXDXLJrXfy`vhs9hcnv zGfo&TT;@X>A5%r;DTK4e|GdAcRk4FT%-c4R?+aoIx;}r~r8MJ}ZSyTyU8v)Rtyaz1 zAoKj^uVnn_iSlD@;+$Gk*GbF{&7Lr@wtJ(yOy`OOpVF^y&Ov|9LGRvFCutQ5nib+! z$UotCSHPyJ{ZI8coLEiY6f-p?ANsAa4=119MsF=FNoU z7X^|K!X7I;RSfi8YdR+jBDO9^iqH4QX4vi6s004*+lz|W-34Xn1k;{NfaHw|P*WdV zT_;DFKBx- zWNYE%&`n4xJF(x;ippG?WoLLMI8o^ASoEEbeCVl3T0TT39(ygxL9JI}qCGgQb9GGy zLiq%Q6%D8x89HDL=sXn*w4g(%dp~0HkCG)Ad&ee9#)y0oXJ?Qh&o{3(dd`KvLFFvy zj4`0=3PvoA`(NIpMi!fTuCG1*bj-5ykyg%KshGiWh;FAKzgt;3!t^59Ec1rme1vZ? zwil|n7G-WtPxajZq9ky@ZtEIv^^h~L0G+(zG5F>Af`PjDz7%{ngnf=7SfnUH6LYa9oJ3L30=c<=bWKfYR z$UsP^5R|4C+xh4Iong6R1yAhF>5$4AUj`*7nFYoB=Uw$0xr?2wE$z}(11jg*vp6{= zeux3?!+9#UFCE1`!f=cRRS-}kZ?&;N zV#1FS#mbg{o4>f1B3uM<@Z=}3=L%p4gQeCl#${CNeSwAbS|?nk8@aWoAVSc`KukUO zhRLmeB(KAr=k`{5CmCHnK6FI7T)9HcpGdQWM!mjmcO3gz{h9^E7&_xdDtR_v$XPMd z65eCjg-$-TW>fKP7-L=t0y^X zRteDMRN=5Q;-=k!kC6xV(+D+h6~&hMj4hVagX6Sx@DD?6_=fnI+(enj%t}H=P0|ab z*6(+lql&TXKCmgr>*a#~0l{u9adMYjA%W|%Z~mt1xQoLt9^qa5BSQWcK8c(|j4<^> U8D1_M6#oktBh2Bi^pN-e3uTIGy#N3J literal 0 HcmV?d00001 diff --git a/client/src/static/icons/flame-white.png b/client/src/static/icons/flame-white.png new file mode 100644 index 0000000000000000000000000000000000000000..529dabe35c671abd89ffb853f757820b87f4e362 GIT binary patch literal 1698 zcmV;T23`4yP)Px*Oi4sRRCt{2oO!5KMHs-p$>q6aX68~R=9Zazlr3UW zYT05bMI;dj21y$Q74%2YhDd3sB^4N!kr+in1eK6TpoE4bYGxRfrO)NLJ=63(`}Ifj z=yl)sox6DNoOjQg9~`*M`DVUl&Y77r%N2=4B9TZW5{X12kw_#Gi9{liNF)-8L^OcW zz#4yzOxvC|1uy|P&VL*Su1edkHUZEd*iTDpf&OXRmuY~mz!oL=O?(e@N!!lM0(_Pb z{3bq4+s;e^yjno;o0yljy_o`-RYdTccpz=NGXZdILXbZKK2ErQW!m;vH9&7*pAp=R zKn>6t*lfuB0rX7UkE#S{2W&6`yBp}$Otv4e$BU<7c|2;%$#@-H;<{s{~X<3*JMbOL@d0=N!1r+_lrd0CCE zK*uoNR3X4SM&N!eW;6rz^dlgRrIq!fN&)UN0`?a$s)X{!1E-BLXH>?EDg_t_95aIT z@L81mxKZY@ICI(zV7(Ej&&yEu8l&v5%VT|&13YL1XfM#C4CVLoWog`B9xtjCpa*bJ z3C!QX#4_rcdtpd6h8M>B~TUS*83<4UJfcyerWjq=w=ADWGpa8~S;jxnz4p1atrnDUL5p!ImY89fvZ# zdIn54nxEkFGQbeSmT94F2?nrAad>@-_H0t@`5-NODhkleSm}B!MVp>d>^T8+PRpi> z0oL}ZI{>*`Q8gscF~sSN@3 zTWB_D4@*U8A4-vkT%)9wQ~|(n5xGJ{nkLbH6OnDsxg8}Gw?Ra5SKYF8*aH}94EY8Y zQAStb6@L)JVh`{#&^4rOtsdYu#leF`-ZZ4wyqe>Z5N_MyiLiGXKmXd;+ z3qMT#QbhU`p|7uqtO4d0DF3697GzUzs0z41NvqG7379M*i%Qh#L}W2ArT|&ZmLL@n zsuR|nNvr^%yNImxfzH;KrlicEVE`4AZAoI9PMPP0XOXv$8SO;El!-~|!s zrKH!1$Ta8NVCUQ%e+?3m86t96ksl!po}x!oe7b-Z1G!3p}b zwTRjT`LfYkD`32mw%a-PNj^o+xmAX~(FxhwWCaWZtY%0H$fhFnZB)|K(UAXW8>ZaK zdkZPrcCHu( z3Kn3BF~x9q0eY-+V(OL`V7nrly;U*bJ|i$Knjybv6UXgcRcP`mnl9Yd)UTg$tjw0K`2iszY zKU20G*qI>r9nfOR1(<8dF9;Tp&;aKfQ@=;Gb6|cNT3fR%lm_Ea_ei6OJCynJ0>G0- zuxf#e75PVuZS47I1X@gI_`RXl+QOs30+A-bS|b2Ewf&jE7~kX+%2sU}8n_JDW#suP zSOC%{&pFl86FyC*YwP*z90Ueb5la1z&V$UIGf;{JV&j4>X|3{Q&`MLn3fIEG2 z_JrSaG*TPA_`keamQZmeokPw9!0d#lx#?|qCSlGfsq7lyIiNm|j#-uQu1fHgkgt=i z1KtB>B#qJi+c6V(-`IuSoCaWaSZ`ZmE%yb*xB06@q|kH0yZp7?FFOwP*PsIP9}$u1 z&be>S!t89}|E|FLr<8K?L#FoploLN)@oWLjM=8_+PnXi4v^jw8eixkfwOTCoz)L{4 zwEe0&{C329{h8uo-ikWk5#LkHHpMh(6`gRQ0N07gg(A}3U+4MV3&)yWwa&S3)3!Ad si9{liNF)-8L?V$$Boc{4B9T_aKUuy2d-P?X#{d8T07*qoM6N<$f|~RoJOBUy literal 0 HcmV?d00001 diff --git a/client/src/static/icons/flame.png b/client/src/static/icons/flame.png new file mode 100644 index 0000000000000000000000000000000000000000..6f77b55e7937dda1b4b91c07b37316317d62ec04 GIT binary patch literal 2844 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 literal 0 HcmV?d00001 diff --git a/client/src/static/icons/gift-white.png b/client/src/static/icons/gift-white.png new file mode 100644 index 0000000000000000000000000000000000000000..406ee042d373d709ce3ed0e5bbf33c897c5b7b52 GIT binary patch literal 1104 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEX7WqAsj$Z!;#Vf2$I`rxsqw#?d-E$%zysnzd8(&?V`jIh=;rDaP#M0{Qjo*3&pDQO+-n@B& zX9vfRk{(ayAIrtwpP8}o9;2Ryp|~sC)Gu3>=<5F6JgLmH%JED$r=P>D=oa=3N;O@l9cHDP@6sG6$_BT}*J)8L3wIX}T+uZ_(uFw1&!0~Xw66cBQ zt}WkirAR!%cgfr1XDqMO3T4iI!E_`hrr?dXQm*(jABA$py523hdZmI_3Uoi!o?|JR z|08_jwx^TKQqMUb2=m<7yZ3z2^GUoLgnpQB`gG&=!*`mWBiL#rPd?_^pp&k5lX187 zwEF(EGY`ME$;6$V>TGymt?uuC`OM{CExxr)(=JR3zah+7u`N|-#XW<_*#&z_L(iD` zFO|x-o3vMzGe-5Lqv!wIKHU*oFB3hlZ;|=jyTm-|SGdQnmAh&qF5TBXZWprG^yZU} zC3h!DOx&*WS0~e-FkIDp^n6m-vJP_wbrs)sfxaGTjjKi_$CYN)c<%DK zq^!xbRO`Ug&PO~aSBW@^9SS?L=-Tsr$Hgc8y7m0NUS^fwZAH)bUrp{YPkJ}w^0lX| z)l1^I1)M4t^&dEJbt2Oj%X_ap7$@l@{1FM6!5sic5%X6s$XqZz)H8+k%J+)maEFsi zv<@ttxb0*-p0!juzcikkE1q5C850HQeA~SL72N;gr>Oniyzrm+QqKvi z7}SN1N=~{T%;Fi^zz<^9YBnwTv#GCI_3liqICk?Z`&d3?d~KE9DZ`^!Go#SP7ztR% a19Nm(+jgc~k7IyE0)wZkpUXO@geCx368AFz literal 0 HcmV?d00001 diff --git a/client/src/static/icons/gift.png b/client/src/static/icons/gift.png new file mode 100644 index 0000000000000000000000000000000000000000..8de8f3f991df9cfa79af61cb3a893ced26bef245 GIT binary patch literal 1534 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 literal 0 HcmV?d00001 diff --git a/client/src/static/icons/lock-white.png b/client/src/static/icons/lock-white.png new file mode 100644 index 0000000000000000000000000000000000000000..29898809ecaa974f8b9b2cf0cf3c5e18a842fc1f GIT binary patch literal 737 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEX7WqAsj$Z!;#Vf*Qw3?fu3YI$zd7n|B?0BCwwZ`Y)xif4Kk-XsaCh7L^TZ|Plk^f`o z-8GjfF!;!4J8|PNpGC+1#hCuux-LX9Ve?E`^UZH%?X4zQJS{lJ*sIT7g5WcX9V#ja^W*%kl8`SLV+`I-Qm>-H2G> z=NIiQqAA*-BC?yq-NtnD@2jup?vI^we@0~N{Jkr!jw_inU)j=bzk^M{X~&a@|KbVJ zDh*2J8oxM-mNCrHWJu>aFq5f(6y~#^Auik!UCWiutP!;|`_#X5i%>5=x9zk~_8OuG ziWXW~)@lSU31Q&pzIyVdW2e&5f6-oFzwd1Kx+;C4h9mJ@wXhNjVE)DUQ#673dx;wd QFpV;Jy85}Sb4q9e06R}FP5=M^ literal 0 HcmV?d00001 diff --git a/client/src/static/icons/lock.png b/client/src/static/icons/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..49e00c1fccc42f01eff451b04ad1fb3824c2aad5 GIT binary patch literal 1012 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEX7WqAsj$Z!;#Vf5+TD>L4z;t_^ZqwtQTa?kJ+QNIi4g z+`krIlpeiWdf~9s`si;F5vDhD!}#Nt1YBrx|GG&;OH@SkX#ef5E23|FHcdX>EIv2o zbza)H+U0LQTenuKzyCk~d!il3WCkUypq%OR-2-`N&wbh$H_OJ9fxqFPGXsP4AF%^w z0o?oF=-ocQuli2!ZKVc{q~z0+#7|?%KtP5f{FhB18 zXzdeSf#sZccAws!KkMP4me%LivbS$67Aw}yWl&Fl^!$>DJmU`a=OMGFa>ebvv93NU zAiI^T#@xUw+p5mmw{U60QTB%4mqg~z>3DRVd&cU5rPfSQfj0h+{``MaCj6lEz_qgh zs+A%e)EVFGzB-#JVc+hkc&%-ZYrI4cgoy07P|(Z2J^iNF(ypaf%d@VZ?fEYk6_N1a zuK!+79=3Fb?A+ic#|H6TbAL^>&8XK|%Bj$G1w&yg(u$hhEl-jA)4=lUd8862Jc_+0doAQpyCQ{Si=*Ij(u zvh#h!uDJz*4}Nvb(tkd8H^)8Z8M9v6^4@5^`Ie{Y=$pCcEY9?_B@`YxaqybYdyjiR zKNpqp&agV$KJ$ovqs5fWhZ3ESm8GJa3OCPqbxq;pu@7l6MjxWmZm$#Nx1`(M(5dvmI{uXMgqC~*AW=Jkr-WpDUZa35$` zovr=b>*Le|of>u=DNGAyON-2HvI%CGr^#?$q#>Q_fHCWXnM@yi7%P@B5F?!L?<$b7 zZPDzd6jyIb({oC5ufecRQ{o!S4|7W#KYwA^Cwq@{i^>bP0 Hl+XkK6zSH( literal 0 HcmV?d00001 diff --git a/client/src/static/icons/notice-gray.png b/client/src/static/icons/notice-gray.png new file mode 100644 index 0000000000000000000000000000000000000000..d86bfa068f589a1e7282af082e0f9da48dcd91e8 GIT binary patch literal 1765 zcmVPx*k4Z#9RCt{2oLg*EMHt8b-|SLKDUq8L`cjFYB2*eR zMx!wZYJdQhi)S&>XeC(bwrE!$j3H{`gGQjO?k-S5Oduqb6$ll0Ai-dS#0R5X6r%AG zQNSAtL3-akpAXHNa?WY#vS;Q@$^0I_nVs*S{qH&R?{?1s27|$1Fc=I5gTY`h7z_r3 z!C){*hk_Q6E6vUO#&>2q%51jF*rqZ7Ai-w@i8CQP{Bv#XlD~u2mWGFH3nl#KPS2W<>!NbI!0ymb(t`f~ss2kZ+V@jC#3?TG;0Oh5$#rPp|U zCkgRPebn0JoA^A?!hrSh9d8ln%>kyg1K1uC)bdg^a(JM%8sn+iHrY=Rwjf49xHt?h!)FemQC#&8J5` z1ZF_v#vQkCn!W-s#x+Q29W%0Ic~#XToqqIcZr)ea*4_3IfcYN7{|py}718JuzxvTD zSHeLw1pw}$Eg+9KAJ|_XjVvZ2_N!NQ^}=>$UeJ_C?Pc&H$MD!L?%E7so?rcPK?KpX zDY5fm5azfB2q#e@EI;4;|Cc6WE}`WBPP+z~=YFuE<$k|=4u(Jsu-RVgQO~JF@x?gmMEK;;C|g?sE+gC1X+R2ig5szi6EX(CjfZt7${( zcG>-%C^vv$JMX~&aPg)Qg=2cS_XF*dCWJm! zsRIDQlZy`nXmgaQEz@41QioCl7?|i${@SuES1OUs>iP4#0DR}Hi;Sm6X(%;-1l{SZ zF;S074Oc;M^=3hLsFa{~6K0;}ED`fqr5gJ7290*s*tXq&AWB4a3i8_ETyanx?A5zO zr2@rQ0F819ARJMup%295{uY$r(kK&U1|*ZIVg`?M)`>!URcUa-z;2J?_+&CwtV)A2 z1G;UV=~WMdPpZ=3q(IOKuX-AEmnsd)3}E8v&Kd#dqfzUMDh*D0d-HG3I%jN`R#~Xq z0~oFd3XlfBPH<~r!5&)WpmGy-eX1Dj{%&41L>~Vipd0WCfcq9CScfZsJv6wr%0h_& zy{W?en@$Rezzrae4*>(Es*;cr19VJ4S%OYNN(|630c8m~38~owv`s)+f=fcGHeo#z zP?i9bkRk(gO+ZGxgC72|n zViVRo0c8m&2`Ml@_XLzBm?R|CfT56pvILccq&^?eUjni-nWgLFJ06$WJ7<_jc|ESu z%_s-(xvaKQ4PfHw@c5-a6Jiwn<7rpH{m3DkXUH@_>Me+vrpjDj1x&b2=K4|%u))J+ zuCD?nbjw^{?iCOg+hwk=0s$P9xxVxpabq%70p@u&+e7YZppZBktBD*6T8F`KgEc?li`8p8D zbHalqlg_Mg9JDbScKTVr=Iemg1_0dUE@4{#(|L-!#J)du?>s=q=sm{rswE1-E)#V^>rEKF z2Ra?-=a`uj416XCVP-r8pxiluKp)rFMJm-Oy*>gvu4d4L#S2K=-vV%cFF%ujnFHM6 zuM~!dt(2}WPxQV80f?=#&JodTIlqwbO1`IUsX%)J0HD4$k^~S1FqjjxIsrsuwaYgv zkd`;v?*V;jNbH&pq<4eS-?1{MK;ei^=Yn2~A!Pu16Av|QYAItDQ;_Q literal 0 HcmV?d00001 diff --git a/client/src/static/icons/notice-white.png b/client/src/static/icons/notice-white.png new file mode 100644 index 0000000000000000000000000000000000000000..3be03d148d57f462cc3082e71e08b258ce4a262c GIT binary patch literal 1208 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGoEX7WqAsj$Z!;#Vfw{A{>yLhoKaxi+}; z_Gd@^WqGA@_rJPx`TP5Q#ok3S#|x3soHs3(n2xbpxL^8mzt{uAwl+Lx7lL1#@Fc7$It^IYEh;@93KvXknjeM!E& zE$Y(FNoF3iq(1($SIOm6+;ef_mw(JQc8|{3^6X)`v#n!e9(!Ee5%(L(W}8>bJou;B zUBXbo6j90acpcL`raDQZf3=lqSA`p-8y`X8Fx@1* zqQ|^3@Oo2Wk}mr_zazK!WA22mxyNuuRqToGg5qKZmIO1w&vVy34&^QIPEssmI&E|; zmhFx2k$raIcV02rb?F=^d|=dkQ{i3oFriP1;q z1Cni0=a}kxUsN3Nay~8ex?$40zAd@jif5uZITU54eDQh`;B)&4!<@Mqe|uTC+!4L_ zvAlSHq7kE+yIQS;#nOgpOAjpUnCCmqYK?tUnES1=c7cCJ*A@yK3iOP3;X7)QcG!yX zwNh@qRJMfZZ!d4gYn(ewFKs&&aadua{7;|t?$5KkmX){ja?6}KwU);$e zWyM2#la2KqGDf=#^Jg~9V7;3yVejhW?2|3#6@DPS?PKr}6Q2~;BWj`9tUh5(QsIKX z=bwubHb{NivPNrx8~c@@E1Z$43#@>QTOdXlka31{>Y4^AnE-D?^$;V*t>Fh!R2CG0 z#Un!;&ho8TIzxDqhr?DNV+n}y7Rcxj)(T}bwOlYw$!|pn)2|@LzkUw>KobPjOR~-# zY`0iY-C%8eTyFZ6y$>Wk<)3lQvSwTrW zAKW;n`MKYL)iaN8e{gqBb9#M{+zo}yV*k~N)f|cIws0Ju8MEr<8fDkdTWWs4xRAr~ zaJk-ZYiEXQIyYWFR4B*{|C_zwh;!BB4-HF<+5g39X5L_ve*8_}?=7c=RA#jGlKMA} z`BqmhTW(c{32{zp_pLg~qD?nn{Yizh~yz{P%hM`MbxjpiAY7EvstxKep(@M2i0zjHbL| UD9(0n0Tw+Bp00i_>zopr0GufwT>t<8 literal 0 HcmV?d00001 diff --git a/client/src/static/icons/notice.png b/client/src/static/icons/notice.png new file mode 100644 index 0000000000000000000000000000000000000000..743c9a70b9d3fb68ad73db8efd48c03ea42739ec GIT binary patch literal 1745 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 literal 0 HcmV?d00001 diff --git a/client/src/static/icons/planet.png b/client/src/static/icons/planet.png new file mode 100644 index 0000000000000000000000000000000000000000..42d9fcaccc58e789b7873f73e980b5cc2a1f2130 GIT binary patch literal 2978 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 literal 0 HcmV?d00001 diff --git a/client/src/static/icons/rocket-white.png b/client/src/static/icons/rocket-white.png new file mode 100644 index 0000000000000000000000000000000000000000..d3973f85a7e0a8a98ca4f24e61b5e6e4705b3329 GIT binary patch literal 1834 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 F{|}8;*Zu$i literal 0 HcmV?d00001 diff --git a/client/src/static/icons/search-gray.png b/client/src/static/icons/search-gray.png new file mode 100644 index 0000000000000000000000000000000000000000..c27c634089226e17d9907de56f709bcb53eca938 GIT binary patch literal 1979 zcmV;s2SoUZP)Px+Wl2OqRCt{2n_FyL#T~}K|Lm?E$BDTl#Dq8{f`(F5 z3TmacP>B$g0(EXiF((NRh$^K{vR<5Bq?C#ZXeCrp+Vy39!6H-w1+iY7C?u%?g*No1 zrKmhaYEi0?iyA@#1)JEpd(Zs);FU`E%zD?}nRB+y{2u3jcE0cIcP?}0%mFMGi^XEG zSS%Kc#bU8oEEbE!VzF2(7K_Cg6tsat^z`gpn8o-)mt_Ni8vuYo9}?(T6;*v0jW!(% z+M^CCCV=+-?eqC!b`^l-0Dlj_j3Pfd2jKT0>=xBEuWf#K)yFPr4Yf%3nAaAP9Y{8t@X?cJH%nB^V-SE}j+UK)?Bds~N= zE{q1y-uKEP&eDDWb&B#a3iBQDjq8r;)a-n2aIqZEzRkcjitd+(ToJ0W{xJAbKX=>ipS+ri!-cntdQ7y%oAPdwA?ozkE!F_5d=OOo*8k zJI5z#BQu}YtyhfNNbOS-qpf^9GQrG&&xL6Z;P}T_0>W&skl51Fvf`|6y<)VqtT;=8 zw|FZwr?dCf@A~a0AMF7!oM*ch3#PP!6pDxEI`o ziss=3_4uPso#}8*J-wxRF1R2ss66|I)&PiQgSQd;9mnyG#eF8{VSj#mR=Qr#KB6@M zCM-}iey&raEC^mNQsLQ0^xlLe6sI#+P5@8@Kq}Avq4^9T&w~vUs&s0U12Km^ZIX+v zzA%d3djN3KBfvCIr$#w2M7^hx=tLl9a_GGT3HeEW6N5d&(%@jv{FLgT@e|GW08%ZE zx}g$g9$$Lt%~}9AcnYNS>>GLmAgFexI(uYviB_Fy@Y(58cLE6Iw@dy%Is+EW6MxGs z8T@Y;ck9%d4(!VBdmAy

+8EVkn2!0364;2w;z=Pyqn|UJop=E9W^dv`@wf^}Nr` z$=*z0m!XdCfjaC zK*Dj(F)0^e-PF-DuwJ)bF*tptS417kIeYZ#40v_ z=jTz4>N+Wf(HBb5FV={~cFtM(aR6AG)$i2zFjPVJuj{eM-+$`P#Q0plQJh6WL zsxi|ZO2OzgIhjiD18}FJbPz%`CZf&eeg=2~xM@uLEEKe`~ZNMmjo(^3r_o?=vGzz+wg# zE3fZrP7x4d^_fu-A^84en)_y8FMux_mO4OT{y{h!nK6#+KGkxWN(jO1C`dtkw$#H?BJxi!~pJAiNan7cWU0>iAJ5%`NdRf@Wx6!69QiyjNIF*;Zq}x61)g_JOR}n-Vz;z5Azab{paMd;f;(Ek;R&7xk!xR!UzHh@Ve5`q?N07WPe!arwKh1zxWZ|?ym zl6#Z)1OQM~OenjvREt@i^XEGSS%Kc#bU8oEEbE!VzHFSzX7}st4ek;{+a*) N002ovPDHLkV1jxdqlf?i literal 0 HcmV?d00001 diff --git a/client/src/static/icons/search-white.png b/client/src/static/icons/search-white.png new file mode 100644 index 0000000000000000000000000000000000000000..fc72464c753ee09af30ba8398aa2494f4259f6ab GIT binary patch literal 1268 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 R*I@tv002ovPDHLkV1kvi#y|i7 literal 0 HcmV?d00001 diff --git a/client/src/static/icons/settings-gray.png b/client/src/static/icons/settings-gray.png new file mode 100644 index 0000000000000000000000000000000000000000..cab09a1b791efff04c8d6b93658602e7350e1f9b GIT binary patch literal 2394 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 literal 0 HcmV?d00001 diff --git a/client/src/static/icons/share.png b/client/src/static/icons/share.png new file mode 100644 index 0000000000000000000000000000000000000000..f3c52e1f319bb731ea5497881571dee07f3d133b GIT binary patch literal 1829 zcmV+=2io|FP)Px*&q+i{Mi)7_o9v)!HE-{ZYA=bqeeZuZWdZx0}mNF)-8L?V$$Boc{4 zQZ#6!6BucFdRYCuFZCG6xSNRY1OR3_r7Fs?dnZ`s$(Ma8C3!V8}&K{HUk)A z=Y9e%vWlf$?fx%CAus+OJM?t8f~i)L~`{E=zP!c?tJ6KHdXBc@KJUT&1`Do+nnU+X#tQ-K5Ev?71{@dhSF4gXwppmiG=AG`Qm8- zhv#q?h1t2FKbZxk9YeJSy#S-VF3ot^xy;kh^Ao*q_vcj&8`sd<>o43P3f zJ}+Vb&=?tbGL=g2&Ymyc+tpDu%SpbTS^<#=SNHBn?94Iw0HA==+Zw_HIVUf;Y~b;9 zD%EZJu&T=YPV$A9RzO1}`A9ltbi-`xX-$fVnq!+P*AfvA?Z|N$C*nGp{2osobgGK*EfPZG9g8+QQJe5Jy0o-UYr7HYg3E^)|1aC)Cy(x(8&jYc!$52d^`={J%x@*;BU#t z(?m`_im?Jj`fdhe=`8@QvdW!^zt&X$3vF$5ez;imp=3R?(agYoIfgx|5Uq|ywa@a+ zvk+VpK(y~B65;AZTHnS@Pgz{T0KdCnzQ1M35=!TrXE2?yW2Im9;j7M8 z?U#IG*+IAfZuo8nx0vZI0A(L9yJQzB7D5GZ!*`QwiwWS6kFY2j_5Wr&@exFr0HS?2 zw^wVs^4(&tTSr#}j5{I-5kR!>h6w#&oMhmBiy^bNnB|IN0j~OP4ukQ4N%x@d=AMFm zH^XsUtx%SBwpIS%H1|@#u>h|6ZZcyMc4?}%IM)`loN)Xi=Bn@JIt=idx_R1MFHJt2 zu7F?@kLN_z$%pa3nxE_-;xaH%>!K%6C)hI1``@d^e?zGXdB*Ytky#ccUo%w(~0koC#1t z-9`-3&>T_^rBm{&xyF>n{hKzX^lPyfI>Y zH$z~i=H4BNoskGvJ4(335ONwC2jnk_}N@5U`#is$PEiS*rwG-kjk+$TN|CPo2ShMc+KE@!7!9ay&zAmfT0Yx;}Pgv;xU>wZsSJ!+isqo zS^-0ecldi1D5g)StQS-?jO_istQS-~%E7nfX#p%KDc_Q}1t4|jY^vR+W$i1sbXdOAhX(ONI4RPlVhpmJfP>jg<95{X12kw_#Gi9{li6p?=cl8`7% TkLqhF00000NkvXXu0mjfQEXTR literal 0 HcmV?d00001 diff --git a/client/src/static/icons/sparkle-gray.png b/client/src/static/icons/sparkle-gray.png new file mode 100644 index 0000000000000000000000000000000000000000..ac49239d2729d67cc13b21fc4d290f4410aba664 GIT binary patch literal 2712 zcmV;J3TO3+P)Px+XzDE4d$ZZ(B)QT1ndu*Uf4_5nzd7IBbIv{I+*`oG!NI}7 z!NI}7!NI}7!NI}7!J#Hly%Q*xNFue2C42xqjk&Zrq-mq|&Z!(4Qh;zmKNeEHGVapb z(_AeJ{QhqY*DHWksJ0-(+0+E?q6ONBI?0C(K+_%r~Y zpJ<_z!@54b9ywKlyWR;*l|3W7zG(5o@rYe~DuCapj2GUdp9U#U0{{r|Z0Kt3S(WuS z+#O&rzHDWa%wJL&E7CU-a+m_D!4ct?H=_X7B>iRr1l zO$iM%4b5`)$`V~E1LINvKd2<0rfCL%uS66283+4(0r6sQKzp*1dJAcYwV=K`ADo#R z-t#(uLqB3*JWAkZCOV^_$Om&;XCKwk(VnhpqGiw!U63)n``Z&5AAlg*0wL}&DRSt0 zdxo#8O?H)G`#T`KNk0-f`6__InaA7Sreo(dHSKi`?|b(R0CNlMX)$g3{N5`Uzgxp} zr;PpYfSkP1r13$5zt`D$N_OqqwlTqUy-7*092>o%Hc6I`tp(Va7-|Ddr=BnO`h5cj z|M>V3bUT38P5iEiCiKGb!B*H-fO!0#!wf06nRb(e>xf7m{8Q63gM={a^M#ahTQa$G zPPOwb7h4Lz%#=Z98$gGdn4b@I`F0du3pSk9Pex?KqNl2!F1CF z7;}5X8mn?7LmzW6zsW5`g( z34MjSOx0kk+cF$a`M}s}(eLjFb@@tK1`fwk4**zXVlqhJ(qO=Mmx_EvI1em1pb3$P0x^;{9mRetM?a~k7b}a`W zP0YehBGk1DN+zH4q%*JG3~;Yid13Da;4(2w5dGBanSXs}=PB88>Q_PC z1lYKF`=^X-?rs1JEL^e#EDm=29;zfxIIb@ROTEa-?@R6`Ebr;`y;ez_a;Zxf6pp3* zMmG0Sk;dQeBK~@%jSm2!F6~}&i6sE6e&q{u8T=^{PhC-Ita7O79njaGIz49~2H=z; zZliFy+C%=8W}6{p6OHTVOO`t)+0uN9KpTSHtDaIByBIYQARN;dfcPh1{@Ntj`~z?= z4|T2hYc=xd>q{-n2@HX8+$65Qbh+`JYp(YFr5bsRr)mN)vxvs^ivai;eBNZSh_Gpv zcW!9y+P1N3<%Imnx)0OuMw!t$i7ONED;AUp`n3nfuRW8RK#)P0MEpbZsqX8KuzG<$D>x zMdAK!XTmVTlg!c%V6YEB#R4dK4j|x|5?xz_K$D7?)j>7`Xer5+_#G7ymD~XY*8oFa zT7v5dhUCY>vD8BzxI^9DEB>w`ex>wn){i%`9A$&gFRcW>Gff;4QxRL;ARN;d0LHZd ztuot0Hw{imx@L&$doI`httyYHkVxo9jIq3t7^OZ1j{z8Td&LG-IV+!r-geuLLw1jh zt^u$fz`_!W>=qCmz1`aVs&gquq+h>)4S5%U))L%bB%)hqPM8uLni@w|`C=xhDY3DiZrq54pQ)IXS4yen2=jxDKE`05dFH zhJ`@8s#||!zy1ZokdFbFY2mVu1a}7mtGB3#Hx=r(ArtM_kCTQRD$*V7a|u`bHG!4C zt0Yc-3H%WNUJIAHZpheuOLCOJi-O&&e_c-f8{>oL8caI@n7u#D1p4`mrupCfzub}> zPqZ)9AqiV7@)>~;3qt|z)v44ONu)l@5-(V6z(@i3db@qw%FkL2wm22h+v6Jm;bH)W ziFpR3+>uP~v>GoakVxn)EOEa@au~fIkvht{^jeeEbVhkuG zk{G;t$j71ocOVCbnH-aoXXP86L0MHy><)Z3XD) zXivM{?&Sa`F3vG-(slj6Yf8!Fa~@{n$0mLmkb7xo=Y=EH&$v8nD*yoWT)pB2V)U7q zKKa%zx$fW}BiYy11N4arUW9P9&KRje=^)I^6zw0n7&1Q#5ws&1ST$Hlo%xdsZ<;J6 z{A+g0+<5?C@tW#xZ<9E>7Au;SP-JO%>a@-|J_tq#oqLq6derWse%dPXl z2Zu)6FIXu|K2y_@*`I!8ESEkR!0HJFiEixnuX?21xV1uY7bF*#c5Q0RnK>&0@PxBN zlCXx^^c>O$v%QChD~($Vsy+q~sN$oB9Ubjy5Se`{>^uNx7G7jcFk+8XxdwA z8cKjbK-)*f&SMfD>shpD;!3KGX(#~>K-LF)85Qjx*Mt8l0R)jFK(*nyc-rCx>|Cq& zaHZdMt_2MzK>mNqc;3YH$3}Vh8c)Ls001C7UU-QWyev@<8d3mvv!}118`8 literal 0 HcmV?d00001 diff --git a/client/src/static/icons/sparkle-white.png b/client/src/static/icons/sparkle-white.png new file mode 100644 index 0000000000000000000000000000000000000000..97bf9830aaefbd233d8d3ab9ab33492cff1785c1 GIT binary patch literal 1584 zcmV-02G9A4P)Px)+DSw~RCt{2oLz`kMHt7QSu@vG``9%K46`ybtu(~N zKI~(yi;!v!BSkVHl}NgqNpySj4Vpb%pxso34%Vzh?Wd$&Dt+* zef@ROymDI4%zMuJ&N=6uedh;zvFFTrp8qp*&Y5{+j4fJELjg?T|8xUq6|Jipnu-=sC98)0KT$*;EJ#b^0esHM>@8YXLjW`b zrI-fz$H%_ll%fSx!BvJ==V%d`BO<$9Is@88q+S2JPbsy}rG4)J9*(*D%dB(k>**LD zn_T+v4q&^b0k%2x(`oV3>B^5+qadI~L^>=*I)G+J?&ey~&2r?!uLcm2B_cA$a=t}G z?#e-jt48*&0hT)|lY}p-JU21{&bPdiqZ|h=aNutgusg>79$=K4sQem+u1r`?iO4-7 zvY{4dN~w~FTnAhmdFOX2rS{Zf*GquYfu5MVaR=~m%<&%GVb?vr^a1Ncq&4RF77=+V z=6I`!Jd}g3kqv2gyMfVqYT~z;^L@ZMMeFw-;6+1cyF5~6rJ>Ao#j5ul;6^>F z8P0b=Q>5G|;6O~dlI}zUX#v`RKMZ&F?J?yS8Ok06CKMBervM7rX{gZsmg~MYT(bjE z`9|R}z&b-k4g%9H*UbWs8m?WNZxo&aEC!AlD)GJq*FRvm_871zpC~*(o|4!1b)vrspri7wzGH&J;4a6d3$sgxtYe24xP=ms^> z53F$IuWkY68j`A_3;^>o@O`sxSQ~qQYh3#-9N==`-3q}2d<)FTP{&N|qY`femu00R z7l0<6+VQ@nla2C>!;A)-u4Z6!0zJop_kdfY6MuCNFacN({G1?2fJ4B70(5q2-%0oh zSPR503e^X|3}9=*fXX}tv=yki4cL@Y^H$q@R3QM9QU>uxJq`VUslc|BF8f*?oUq(D5joY7&ny_DH74O;##)jCbfkQIw*%9P)?Ege zu19N0`L*u0MCq~q7DEPuiOs;-1!_JQcp-(}L7gN}nC6NA)GZ&~;`4m{SdOOjE5WtK+h2c}eE z^HR$KObNI%n`k_x!UFm?z-cLLjRSr#*s5uve?+4f&`oQ%ucfeKn%Q)+&_AN7uy$KZ zYT|&)h5iu@u-tM7&$L`O+wuf0%r^?p0bVv#WS60QlV!m@ZXa2|2GC;I^#ZUsru^N8 zvfroazylG0D-Bs~KSno8>)#V`$+$4`WZh*HE9;=3}DatnHxLWzWHee6=^5ZLtcTa#)YEVSJj5)R6mmgnIylVi> zhcU-&opZn?zPxXLn~2p^aion^>ixl@e&H z&{yg!VpC!p3sz)z?wz^!{Q3vG%`$WE>^#oQ+)3{znLpjsHo%gqX}Tdu z*EyVHbJ*eG$$e~^c22X+1%XBzP%c6-z5tt?dT#LKDQiA}U(3`N@628jXTGD`P7%7gnop@%5o+ z0sc*)gb$pN$)RbMp@wDLS>m>Z*g^t8S2guI>2c2+`j3_+)iZ#pzSpiqhl zeT4;Rnt0E6s6NJ3hv$1hd?lZ6XUsPNoVD^G@bMNcd|@^VH3#Dum_D9Yba+xwoBh;N z^so9_Tpph90V0!qIKS}$fC%62>7iUIMFxPi4n%c z&=C+GbdoXiBa!9iZ4ynDf~UG=OgGyF^A1P52z!Q7;gZ6@QQg?fj2j$8Hh~i9er-sG zy)y7j4=C371U{}Qn(NS$PXJm3;D0!XR0a=36K14pgC*e!2NZqs0gFk!wYQf(EVaH= zivFVp)TID~gIMvB6B;@MC6gQ+G0jIAxYw=mRfTSITJ@B|sAJOrR0pvj!VgxMDT{k|JWBdfbT$e04s8~GI8~#N#dLkX>t+08QYvh-3G{9Ke-yeRf8a@1Rqi&5)1QX!vt8IJ$80yuYCEykS&QCdGo(=plx;(R@T3wY;*B%hjvkQpr zZU%k6NVW`-dR;mdbT|yDoTzSm1;9^>A8GOdAh2erKX^cd(F9q4rUd9)gWI(~*TRnR)XV{0U97x$EPFG+tC(@^ zM7jvr5+LpV(dcH)&~*-5Fdt{eT@z$w5MEG#O|xfdd&kF~WFp-G$t2Gh$!3$x;x2$@ zmQdt<0%Ga@@Lt)r6eFsew=i=rfRB`r{TLB#)zr|o?c3?3Ogp4GAdxUHwILp2P`nK7 z-D<1$-GNPXLY5un5MRyH?O}5r7^_MxL*M~clt+t&M9M*m17b;@DNOU7GKxGK0KUVx`ZM+J9(O6nd<*mE4#8zgy(mfad zI{;;o?08Uoh51=K$NK@CTO`{{L87};DeZ4Ebr+){=K--qrjtQWFwo+X<$aKHovf~( zNMt`}LF@xC)g{Ymh4`Mq)TERQedW-w1DR-taS;nVQ)D;kZO z5kRYBhX2O{830K4hF@z9Y4Zu#cxEqGz+@Koi%~tZs8St~MCKZ>*yqytOu+iqkT$=O zArVhpONl13afV1a--y5n0+^TH9)7c&I%6x$&#=X>x*Wg=g{XZnrR}cDa0z(gRz$i# zG@uaO0l?0eG0?&`_9T<+HeVF5Y86iwIey-y@hw%MC0=NJc*Ow#FqqPILZ#as&u$r) zjF^VQix|$F8ZmbOnBySILB-9L-m<6#o^b%i4$uiVo;j318wRySbYta2bwqUI9+$9k zM%F>UOgu3MJEg9<7q`loJ3s3ccF$PnU zQXt;pAPTY&j_)XTb+KZR7;o9UsA02kaH zVu%HuOY*y0mI z%C=uqspM~zXll6RutlShKM8ka+nL2;j84LXy`h~%

f$n@A$_@#StJE17AWojpzfzy)!^xUKt59^g?9odHntC+J0)DzvbWwe zl>;_!rqdwz`Px*8A(JzRCt{2oNa7VRUF2j|6N-KP2EPCsS6NdCQd#` zOniY4;tQxDfoNoQ72~1h^dyXVx_1Oj)Y zEi+dDB{Krf>3=*jYv%(1!Fx_0TxW?X0hCMtY#2FDuxwDtOvWW~35xW5bR(zIr}8+( zAeVDNTVVa)MYJV^k{AJ;PH)ch6phpRGexo^6x#>D%mlzhXL7!wNx}93M7k5Jz*wC3 ziHyaO=!f?e#gss79{@7hzxmDv1E$Zl3R_RYJw1tviIgz}z-h9?q_?8VYv%mB$jxJ+2|8l({>PcheQ5t;>rmk8vV2;jq}H|Y?fSAILe(1 zTQ{#iDWaCJIUs|{71u)N0|4vkN>R0h1)#a<2*9UB)w3~!TbljHL=_VjKx>n4$VINZ z0Q@4VrrjXm*c`3=0g*L@-iC%k>;IagY0Ci|5?R+K9CB&eieT{36C#Ti<(n`wQzX{E z6TpkbsBbw$JKKW39Yn;!ON%ZzosMYyaRcZp;mLVY0~)us`1cn}py+Eso!tYsXLS7_ z12x6g5(xpP${dchtGT}}u^a_O-Ka@LNYnz)NEBalXWviiy zM8b9AwDCSMp0rBS3{2R!uv%;K`FtrW^ekVFjzs&nGx%*AE(99TXnQ!=u-i&eDf

(4T>EjkNl$A>Y?l3rpLOiAD$N(xiW3YP(S^gV3}!vi@5jbw@1H`*9`z zJNXGf_e!$xjoYQw2Liq^N&5EKl5CI0_W{^osVHG@IOLbUTV|JuaMWr+GHBdt&6%)E z01sI$$i$Xw&V=*;I(ia|0Mts5zIjSpD$)ZmQke&3X>*!a%hJAt^Z*E0B}Jk^|s- zr0QQ}rA%>Y0d&OrS6=qK2Q$1h^(q8+d)Izx9|UOu7%1cxkPbUss13LHcZFO0yBsdm z0{G5sHO}bLZbu!mgu$kkAI<{U7H(oaQ(?Hns}DXSsP2hnzVe*IPfLPH)Bhx?DSd zI&IJUKKEA4uLDtMF6|AHvn(NN4wx-~UjavBI8=YM5Rs`%%9i%%z+OP_18{5B#>`R{ z1!VWZk}^q|7^FG#Uv6-tZ)(j3Px(21!IgRCt{2+)rp6Wf;ft-%}_kQQJz4wqON`)st1I z+CUZ2Du~sdJSZ5^RuMcZf(RZZ7Z1g|f)I*$Ex8C%5mfXbLi__NrB)OgG%+T$DD~^* zU0j;YW@h)Dd1iN>4-90(?)yCRy!*a0`_Ar2A%qY@2qA*oM7J1gm+q=lUAHpGIY88bVVv(fAy#LR3>(h*5>x!ZR1rlccj-?FN7DFS#R zotBR3!TF-3Su^{fssNR*1p#+TIwfgO)v+@BHNESV&)Mh?l{asrcus z4phrxT-uRClIH7(H>7z*wWZiL#CkRoR|1YI&WszJH?i~M*y(jvG|fS z1>Ei!-+c;lpM$+~hz@Yh!Sxsbtfe1Vds;WMtGt4{jR4@j48?yfZNr!V?j5p?fq70r z!<+(sBx%~r=FDtP(zK-0PPvRG;Ijc1(0&It{p}a<9Pq~g?Y8R{2kDL{|CTJ$Nno-- z+f%^%CENaI6z{Y$FyLs3;+z9YFPs67rq>qrcr|<5Kz;9dbAh{MJ?cXX?(u6|fh0smQ5i#d*vCH+bd{LI@#*5JCtcgb+dq iA%qY@2qAPx)>PbXFRCt{2oLh_&RT#(r-?Y0kOS4=Ii%O7y0bF^I zka&RviMnV)0`bL5WVhXJ3k@%Z7bIfPM2*OUyx`+BDF&m3S(n_ z%(@_Ktzisl1I=K2^!kj-2Y@bA=`}%1P1A8LAbOA^-h@2~z`Etsl+kD2oqLa_gse41 z$F%@&vb<-O$_Ic5UoF2RV5ucKE&&82=Q_t868^erosQ~)V*AP)TnE3+S0>br2gX0J ze)rE+Yjji#@Eq)4sEq3%<&PURbW96KrMPR#u_wS-GJi>)jF{HS(NU`S<*gMurUgt@ zoM#uVd;p;3CCAzAhbI+uOakC>-2<%0^_P{VBf6lW6yM9XeYt^4JTlEy`BD12jkc7~ z5iNjiXJ3Pt7;v1CHrY{1;1j^i1aQNZ$;^!=O@zQFU^r0taVUTFl7lyBzUFm+85{8EV;etGVd##qDv}5DH{VE*V%eUR6YQMou$fR zsS4C(be{wGimG@4=0a}FJg+L4vIL~lG+~9UM*;k-DrV3i;C#1XKBY3I(rM_}82z~< zVs0VAS(R~v01?hEj+k51Y5GfPT$RCY!pt-@Qa%c%SKATaZ;1G4E^QqlB32$-b-X8>B+ZY{O&>Y><0`c$~(!$X$E>F^k?A)5uVFutS|f)7Tb`C$L;%_Ug|SoW-2$<0Ad@kF?ILqC5q~4)0l=3L(|lO+#g0I1Ne;$K1u*ULRg^ea z$n=QsmIY)Wr0c#*GTrc9a~6V;0LC){suG(2Lp%Y|6t4!bPC$BnO#gk$q`UL)Hk!ig=I!crC#@^2>xKw_hK`%65scQ6@OC?_9 z2>UGv0Cd&aQGez0C4iFoY|rQQl&P8$7#b<< zg@?BpbPoW)Fye)Dch*l2kld7yrg$}Mdv}j%o?d8b*-m1K%2vY*|B=biKT;kv8jVJy y(P%UpjYgxPx;c}YY;RCt{2oPTUn*A>UV=h-nKjtKz@8KqstSjW0< z6m9F;sbh`k)-@VqKrp7-vW8MZutTudvLB+AwywXVVu#oXq=iLGMQf)4Hnvj1%8zx3 zior(JGS+R-A$96X3FL3jevfTH}=#E?c!-i?5^w!QYwo~}*+ zg(*#aQ%XvwRaM>JXHsaRBJW1P#qRD0RXQI4>=$~wmYWb#R^(j*6yk@q+L(DoZllSb zyt)O2Lg8CujK62nu@OGw@wonxyQs1wuSP(OY4wmgz^dFtqYrsA0$N&*7xcwWp9fHq zQIXMUA2(}yP0g3$ISVce@@52Fmfv4EWak3_VajFs^n#oOr2~1B09F*W3HBVoAE)b5 z-b}`MaSIBx?Dz!7;vc5U33!T$tYH38N|Qpif*IbLCI87?l%pdrMgYg-kEk|Sc`I3z zEh;S%$K`6n1Ji*qp8#ei0BW6i$XmPothaXgSpa`jxl%o8V~jDMfWYSV`C#0VDwjap zuD<f0=e6bOKsh z+DiZ|NtF|!t-89r|LWg^uj%D<3sl+Gw%496A3Kcc1YC|ORR9W7<&q?AOOuf$O$MOw zLT^`6 z6ve?3Ihff2A_tf!0PFzBK_CZM8P7ze036Am((8<|iUY!nTE2;}l^KESOc1{84~A1M zbpeP1h!S%bNV>w{D8Nw$^%7AO%uynWlEP6%lA;#kZW>P~*RF7$&$5eB-j0q0LhavY zR@(HmnF_{>Q-wDg?3Jkajerw{~&Did0da`WlpzSP%!wL>CDOtdf8Khz^`a^ zJ^*+;i!XqvL#tgE7(Z-$amNh3%;}bZ1p4pR*s0PFqUVRyVas!d#sGlj_(Z*I>6QS= zPH(Ce#`J)IGZNzmUXSZ9L+SeJDvtxa5`?ox!~)=SeXZ-HUdD8t145x4pNw(*H2|Me zRbWIx_$LvLfhh{0mw`(}93_^bL>y(JUMZnOWvex6EmWcox*>Y)ntvp;wjL~u@_C2c zFFCBT>`*L{gP12UlY>cdFq4CTVgM6B>|n;7V9Wx+p=OVOlQyZa!tMU<2cwBc!=}D8 zHMJKHC`trCg@%d@px#&KYS!C#HUvTuH#0Y6SQ+jaCrQiPZs#RE{Tfn^W@ZY6B2NSO zaR#;4xwlVSRbE~$8xg~BIe74(^~A|@FK86}1|fRd>vnD+BDIBaRv5Y;&=3qSRe(0l z$(eA(2HST$p2Zi8j6;i1Xy@b@V!Ofks)kCx1XQi7bG>R*42EyWG;Z#gn~>$-F)&?2 zlD7?_7d9M$Z6#<4=HZLZOsk6hJ)YHJsNd}En(4->X)s1$**%O8qt zFglh%F!DGUy8slawKHM+jb+k3wY7^4SyHEx^F84}FtVP(&typctEI)$sz$r?)S{*3 zw?%#NzL&w!8fgk7TJLi&+o-4Cp)h4rJ`fB)z>J92dKf$S?|p7w0eiq->(fc! zreb;mlvv^$Dq@*tI&>QWlZ#6ZC9AO4!EwG%FCU}D)JYhF%dq(>CC0;RD=G#uUFQ$B zf1a3Y0Dy^Fe09!uwREeh?(g#l!-oM}cT$d-=L1MTRhL6dPXL1+NR<)rhL(Qgo;~B_ z_j{iND?TPzl2%r442Cxrd}_iInu#OY59XAkisp|1Q*S|yjo~=}()L%`s(Q~J{B-=8 z-cw*a23+lY0Dy&n_41kCQ;p5xyVS0gU{SG2QyZE)zL3?M$P!Z%AX`Z7Kd{@odg*nQ z)3&y~c7HJ3BqwkLz|0ILXUYj2@dv}f)FX-Y?j?UxJwK|*rj7!0Ct-%#o8N!!&eNu* zNO@<^#rFWL8RnBa0q}J8Tzqd+Q>6TV!a>A!nF72QYsvO-9&w#&tpoUOnp2GZxRCxiUnKF0| zgSP;fHiVgCnde)xROLe+#$(zoNCD4R5%4!OUz;Ks-M2%BQCe0ph=Ud6*f%R6sJO5gI9@oz$i!_^<539AewjMP0^9?GP zvI=Slb$mr(`9Sh;R5uYG^Lkuca~VS*6kf@UCjpcJ0Eiy)d6xYumodQ9C*hX%n>o%4 ziVBJ%*`9pJhCrzOZX4lWZnyIzvtkek1OkCTAP@)y0)apv5C{YUfj}S-2m}IwKp^=4 Z( literal 0 HcmV?d00001 diff --git a/client/src/static/icons/star-white.png b/client/src/static/icons/star-white.png new file mode 100644 index 0000000000000000000000000000000000000000..1e8a615ecd8ee772da80bc87afc4cdc1b8f70365 GIT binary patch literal 1436 zcmV;N1!MY&P)Px)MoC0LRCt{2oLPt*MHt7wk{qkMCL1@#L?j-GQ7|zE zW86F_1`jY2*Oxpfo-xED_+mgrqbMjE5hQp#sH~vT-~~RI_#h!3@gP1(a8Y5EfJ@L2 zbk}6VsyTkX)R3mPs;6hRrmJV>`$5A@_1E?N>znH8?yssxL{St)Q4~c{6h%=KMNt$* zQ4~c{TmZemR^Ub8lGu9M3gA3oloR|1%#W?BZ2;b2JpHkCwFN*YFy{CHMuE=Q`fBB* z*aGU{77^)ily{2A%@Opp7r?iU-e2MC*!o%;U_nOkD=dhuvlg0d2)Z|i&AI|~v>(7^ zU?j&GZ~&MbTW?;P=?v%-k?A??W{SwF0`#;Oz;;J({{z_XDE~ON-j)KG;~8$A0-kl0 z9|PvZ*4;7yk9&TAbL{5gk=VLh0>A)!9Rc2%u=&EVe{WpnEdp?jr-$E~u({J?b5&gR zEr3lqrx^u05;h&F>}Z9z;;L_Qfax~XoaPmW?W+#Ev1Uxrl?LDe&(LzI!}fBI?R|07 zw*bH%M?m*v*bg}DcNeR=HYSCx5I9Xl7K=!?h@6_*rlr20EFv=!-)H1-@mj{d*G1%o zRM}+!sj^`aIb^qC5gAK-KbZKO+(txXz!-C&j<6I1aGPfs2uXK_|DFQ;KVNKAa{`>{ znW+lNJ2`Z`-(U>@XN8Je$Yk7d&rDTF^7Ot<4b~ubcr6#e>7K+`2z~)>uR+V*o~P#! z42F!G30WC%wuo#Kk&8SgheTvhM7~R0@J3S?yKxsmW6VEw>gmXFBhZy7TP7m&?PnJQ zBC^sLGgObBLIApeZ5ci22ylOFooRseR-2D)Kv!&?xv&YeXR4ga{Xp>B;5xKUvRblu z8VEXC6auii%AL_yKu<({JywH{F<^BpeL(=00Q)i;`5Uk>g06+uw~u|mlA?7r1Yj1h zJEO6OfxZIu^jV)ib_27D(o+b)6ktn6qmKg{3()bH_2pv=Fs0Etq5(W)=WKnvm(2AC z^m_{MQHI}Rz=qI%O9^m2a4;hn-veg^^f%kq>EmzU+ED(Q0^q_bPpdlu`7yv}8GZ)a z_T2)YyGn3o2Jqoq(gN@)kY}-1YXiLP3DC3vb$5H}elv{viBXoNi6f4(AB-^vCt9&F zX2j-j(oDGS5R~NyxXw`)0A@aN4=C`2)f!)w#yjvZ`MJ;BH$&vEy}Vr#vB%rT{(TX7r?NiXFi%?KNWZmIGV6O z3T(Zjux0>Q>=~PvdF*?EUp>cQ_w{;?^)1}GxZw>gE5HUvK!($kA9f6X zxk?hP!g2fe1)ZrnjA#AmgM~M|EC8Q40`fu1W(6>mBk*a4fEB5_K6Kb^FTCMp!P6tx z$%!Xe*IyFUN`GZB7jTbb|7h$uL;<+c6R>lEo9z2v)p*!$)%YD)11{0ZEZQRf=quR7{&ufI63cQtwL zXp(Hrlb*Ko&+pb)_mQ@Gk;6`k=Vb_}IdNw5KegOF-PoAcmJoZNo`=m7>6!LS+nm=| z*jAPp;HN6V_z_qd(BD#9XQt0k{z^}kKzQ>&0sQ)2_=fRs(X=kW&yMzg4xC?r?+bu0 zJ-3552EP)Px;B}qgi7_5+@kw)wvR*B5c%-h*AHmyw)E%iqVsUp9UHX0h5y6o<}+1bH1kkm8+5ouGg zDXB#vP%CN;O)v;-f4S3j-+Mdn9sk%VEHm#-cX!^KH*4M0%sFT8+57Ij=iVJa zAP@)y0)apv5C{YUfj}S-2m}IwK;SDBbO6&Op_GoeuzQd`ACv03kKRj7tCk-EM7cBh%by8fQj5 zf>QVB*IOq3R!=7s#m5JpQ2yS$s2XENjR4!kx=ZO;CbG?m#v5kL2*_qRGGZD-U|e2b zlTWwF%DkQ)8f#W;jWAXU-{GUz zb*iJc`W;`Rw0r_=i=B49BN2{1eeq2Q${qkUH6Uj9U306@(h10B`Emw*qgqbDfTodt z@x7*z4#JbQ7HCa!Gq=^sTZfiTz=&z207Rtyh~m2XG0X5`oWwXq16cMK(;}5<@CU zJpRfdE`?y953zDQm!qLZ`lmKz4@s(K(q?oT3)DBsSYUv*nech!lO%sW9dR1pJdAr3Z7-KVM3>&>wwS0Ef)!gntll42%+phKczCOCL;nkV3-@E=6INnB@{P zN)y-oQ$bZ(AwrzUh?|h&1d7A9W?x(skWFzWx(v*-L5PEK6M#hw#9iV)9+Bi#xg5PU zop@YqddvFteA)1SjlIlR<)TssVN+34c6sf)5t3`c}B63_AH-fL{ z0ZG;Prp-LyHaQdV5vfi2dS&&}bW$7mN_#}#&rDx%QPCx$R6&!U@+$`44`fo^<}#~n z{RW_U&Kmrd6qZ(bIk8QKWVi7tyKFxT&^0b`y&VdNzBbSoeZ|i>d@n@|^hIBhV#+dr zk2!15HC*PSsV-x+-?3aI)n%;aG9Rte`NyP~vdr(!N3&`{h28Yt;swer1TH$YGce1t zd0(>2*y?vIovLx4W%E7;W;wMJxVU(Ma!X-1`Ld+0_19z)X=4k(54$Cw2v3C}r>48~ zbR(PPStF+LBp7ZZ%?7Zgm{)dq>Gx6uawwn3=y#BXy>9Dm0I$X(@>hF%>0K}UsVdJM zW8*mhH@c_{6JgDuru@zee^Vila&)m@c`i(JQ=Q%Fje6N|IAEDN%uHqId+K!lNebh$ zL3BQXzLuh@++K$MYi9bAQ+t~%cl7j7L)A_9D=h$k7usZbO-~Q~rx72G;Q4vLRH#%o zZ%wG9MKIl6-9942Y}7~C#HiyPMdCtHiv@mK)*>fjjwR;Ti#ut!QOlY}=Lx9YV{qfP zy`VJ`!1zjyjBvm+`_8s%eFi|_2}ox7S^$fxm#ckU#k-scjk@o6xB2LM3y%c3T~?xtJV zP9ur5ei%UYO}UkT(jqtieTHkTSZ25k- z#7PnzuxClTErRwbuAd1HVGU1eyT0Y^T4#^ZDM$hbpJZ&D@qy^~b^X5il0I8^l7eeWWw3 z@2tL(IM5&YqtpFSyUc-(f!6b zZSM<-7=+(eD|5h)pUBO54u*k6MNNLBsL79n$2W3^-1HeV z*JkXuz&``vaI3*FmZlKc2uP$yUjoQw#o`8^C1CSlK9+N|sg7pAw7vS0?;H)m@K*{+sX~ABiBdj3i?h^wV);LO|*3|FF)v|vq~awNTiMX0se`TPQB##NifS$a{A66elbG0zUXo1 zPo$Wae@Y=NA@E_Rc2#A6|G&Y3U{*n0=I7bA4^}QmohOO!9n8ggn;AoA*XUZjGh;xI`+#JYuZ3aU8I$F`joy4{gj8niX2}k}scH01P%#Ju0)apv5C{YU tfj}S-2m}IwKp+qZ1OkCTAQ1fj@jnhkREHGyuw(!L002ovPDHLkV1g#MjjI3v literal 0 HcmV?d00001 diff --git a/client/src/static/icons/trophy-gray.png b/client/src/static/icons/trophy-gray.png new file mode 100644 index 0000000000000000000000000000000000000000..43f064304f3829f6df522a12797fa65347e48908 GIT binary patch literal 2254 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 中 - * 此文件只保留 SCSS 变量 + 工具类,防止重复 - */ +/* ---- 主题变量 Mixin(唯一出处,App.vue 引用) ---- */ +@mixin theme-light-vars { + /* 基础色 */ + --bg-primary: #f5f5f7; + --bg-secondary: #ffffff; + --bg-tertiary: #e8e8ed; + + --glass-bg: #ffffff; + --glass-border: rgba(0, 0, 0, 0.08); + --glass-hover: #f8f8ff; + --glass-hover-border: rgba(99, 102, 241, 0.15); + --glass-strong: #ffffff; + --glass-dim-bg: rgba(99, 102, 241, 0.04); + + --text-primary: #0d0d1a; + --text-secondary: rgba(13, 13, 26, 0.68); + --text-tertiary: rgba(13, 13, 26, 0.48); + --text-muted: rgba(13, 13, 26, 0.25); + --text-disabled: rgba(13, 13, 26, 0.15); + + --color-brand: #5b5ff8; + --color-brand-light: #7b87fd; + --color-brand-dark: #4f46e5; + --color-brand-bg: rgba(91, 95, 248, 0.08); + --color-brand-glow: rgba(91, 95, 248, 0.1); + + --dim1-color: #5b5ff8; + --dim1-bg: rgba(91, 95, 248, 0.12); + --dim1-icon-bg: rgba(91, 95, 248, 0.18); + --dim1-border: rgba(91, 95, 248, 0.3); + --dim1-glow: rgba(91, 95, 248, 0.08); + + --dim2-color: #0095ba; + --dim2-bg: rgba(0, 149, 186, 0.12); + --dim2-icon-bg: rgba(0, 149, 186, 0.18); + --dim2-border: rgba(0, 149, 186, 0.3); + --dim2-glow: rgba(0, 149, 186, 0.08); + + --dim3-color: #009b6a; + --dim3-bg: rgba(0, 155, 106, 0.12); + --dim3-icon-bg: rgba(0, 155, 106, 0.18); + --dim3-border: rgba(0, 155, 106, 0.3); + --dim3-glow: rgba(0, 155, 106, 0.08); + + --dim4-color: #df7700; + --dim4-bg: rgba(223, 119, 0, 0.12); + --dim4-icon-bg: rgba(223, 119, 0, 0.18); + --dim4-border: rgba(223, 119, 0, 0.3); + --dim4-glow: rgba(223, 119, 0, 0.08); + + --dim5-color: #e51c1c; + --dim5-bg: rgba(229, 28, 28, 0.12); + --dim5-icon-bg: rgba(229, 28, 28, 0.18); + --dim5-border: rgba(229, 28, 28, 0.3); + --dim5-glow: rgba(229, 28, 28, 0.08); + + --color-free: #009b6a; + --color-free-bg: rgba(0, 155, 106, 0.1); + --color-pro: #df7700; + --color-pro-bg: rgba(223, 119, 0, 0.1); + --color-vip: #e51c1c; + --color-vip-bg: rgba(229, 28, 28, 0.1); + --color-hot: #e51c1c; + --color-hot-bg: rgba(229, 28, 28, 0.12); + --color-new: #0095ba; + --color-new-bg: rgba(0, 149, 186, 0.12); + --color-pick: #5b5ff8; + --color-pick-bg: rgba(91, 95, 248, 0.12); + + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + --radius-xl: 20px; + --radius-full: 9999px; + --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02); + --card-shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.08); +} + +@mixin theme-dark-vars { + --bg-primary: #14141e; + --bg-secondary: #181828; + --bg-tertiary: #1c1a34; + + --glass-bg: #1c1c2e; + --glass-border: rgba(255, 255, 255, 0.09); + --glass-hover: #242438; + --glass-hover-border: rgba(123, 135, 253, 0.25); + --glass-strong: #262640; + --glass-dim-bg: rgba(99, 102, 241, 0.06); + + --text-primary: rgba(255, 255, 255, 0.95); + --text-secondary: rgba(255, 255, 255, 0.7); + --text-tertiary: rgba(255, 255, 255, 0.5); + --text-muted: rgba(255, 255, 255, 0.3); + --text-disabled: rgba(255, 255, 255, 0.15); + + --color-brand: #7b87fd; + --color-brand-light: #a2b2ff; + --color-brand-dark: #5b5ff8; + --color-brand-bg: rgba(91, 95, 248, 0.12); + --color-brand-glow: rgba(91, 95, 248, 0.15); + + --dim1-color: #a485ff; + --dim1-bg: rgba(164, 133, 255, 0.12); + --dim1-icon-bg: rgba(164, 133, 255, 0.2); + --dim1-border: rgba(164, 133, 255, 0.18); + --dim1-glow: rgba(164, 133, 255, 0.08); + + --dim2-color: #5ba4ff; + --dim2-bg: rgba(91, 164, 255, 0.12); + --dim2-icon-bg: rgba(91, 164, 255, 0.2); + --dim2-border: rgba(91, 164, 255, 0.18); + --dim2-glow: rgba(91, 164, 255, 0.08); + + --dim3-color: #2cda9b; + --dim3-bg: rgba(44, 218, 155, 0.12); + --dim3-icon-bg: rgba(44, 218, 155, 0.2); + --dim3-border: rgba(44, 218, 155, 0.18); + --dim3-glow: rgba(44, 218, 155, 0.08); + + --dim4-color: #ffc020; + --dim4-bg: rgba(255, 192, 32, 0.12); + --dim4-icon-bg: rgba(255, 192, 32, 0.2); + --dim4-border: rgba(255, 192, 32, 0.18); + --dim4-glow: rgba(255, 192, 32, 0.08); + + --dim5-color: #ff6c82; + --dim5-bg: rgba(255, 108, 130, 0.12); + --dim5-icon-bg: rgba(255, 108, 130, 0.2); + --dim5-border: rgba(255, 108, 130, 0.18); + --dim5-glow: rgba(255, 108, 130, 0.08); + + --color-free: #2cda9b; + --color-free-bg: rgba(44, 218, 155, 0.12); + --color-pro: #ffc020; + --color-pro-bg: rgba(255, 192, 32, 0.12); + --color-vip: #ff6c82; + --color-vip-bg: rgba(255, 108, 130, 0.12); + --color-hot: #ff6c82; + --color-hot-bg: rgba(255, 108, 130, 0.15); + --color-new: #5ba4ff; + --color-new-bg: rgba(91, 164, 255, 0.15); + --color-pick: #a485ff; + --color-pick-bg: rgba(164, 133, 255, 0.15); + + --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + --card-shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.4); +} /* ---- SCSS 变量(方便组件内使用) ---- */ -$color-brand: #6366f1; -$color-brand-light: #818cf8; +$color-brand: #5b5ff8; +$color-brand-light: #7b87fd; $color-bg-primary: #f5f5f7; $color-bg-dark: #14141e; -$dim1-color: #6366f1; -$dim2-color: #0891b2; -$dim3-color: #059669; -$dim4-color: #d97706; -$dim5-color: #dc2626; +/* ---- SCSS 变量(方便组件内使用) ---- */ /* 全局排版 */ page { diff --git a/client/src/theme.json b/client/src/theme.json new file mode 100644 index 0000000..dc1f2f9 --- /dev/null +++ b/client/src/theme.json @@ -0,0 +1,20 @@ +{ + "light": { + "navBg": "#f5f5f7", + "navTxt": "black", + "bg": "#f5f5f7", + "tabTxt": "#8e93a8", + "tabSel": "#5b5ff8", + "tabBg": "#ffffff", + "tabBorder": "white" + }, + "dark": { + "navBg": "#14141e", + "navTxt": "white", + "bg": "#14141e", + "tabTxt": "rgba(255,255,255,0.35)", + "tabSel": "#a2b2ff", + "tabBg": "#181828", + "tabBorder": "black" + } +} diff --git a/client/src/utils/api.js b/client/src/utils/api.js index 34ccf08..b0f5222 100644 --- a/client/src/utils/api.js +++ b/client/src/utils/api.js @@ -6,8 +6,8 @@ const BASE_URL = 'https://wdkj.yuzhiran.com.cn' const DEV_URL = 'http://127.0.0.1:3001' -// 开发环境走本地后端,生产环境使用线上域名 -const API_BASE = import.meta.env.DEV ? DEV_URL : BASE_URL +// 构建时可用 VITE_API_BASE 覆盖(本地 docker 验证等场景) +const API_BASE = import.meta.env.VITE_API_BASE || (import.meta.env.DEV ? DEV_URL : BASE_URL) /** * 通用请求封装 @@ -79,6 +79,14 @@ export const knowledgeApi = { detail: (id) => request('GET', `/api/knowledge/${id}`), + /** 星域统计(公共计数 + 登录用户点亮进度) */ + stats: (token) => + request('GET', '/api/knowledge/stats', null, token || undefined), + + /** 检测题作答(服务端判题,答对且登录则点亮) */ + quiz: (id, optionIndex, token) => + request('POST', `/api/knowledge/${id}/quiz`, { optionIndex }, token || undefined), + /** 收藏知识 */ collect: (id, token) => request('POST', `/api/knowledge/${id}/collect`, null, token), diff --git a/client/src/utils/dimensions.js b/client/src/utils/dimensions.js index 7a5e9e0..aa59364 100644 --- a/client/src/utils/dimensions.js +++ b/client/src/utils/dimensions.js @@ -1,6 +1,7 @@ /** - * 维度配置 — 5 个 AI 维度的元数据 - * 修改这里可以调整维度名称、颜色、图标等 + * 星域配置 — 5 个 AI 知识星域的元数据 + * 修改这里可以调整星域名称、颜色、图标等 + * 真实计数由 /api/knowledge/stats 下发,本地不再硬编码 */ export const DIMENSIONS = [ @@ -8,72 +9,66 @@ export const DIMENSIONS = [ id: 1, key: 'origin', name: 'AI 起源', - icon: '🌅', + icon: 'book', + tagline: '从图灵测试到深度学习革命', desc: '人工智能的诞生——从图灵测试到第一代探索者', color: 'var(--dim1-color)', bg: 'var(--dim1-bg)', iconBg: 'var(--dim1-icon-bg)', border: 'var(--dim1-border)', - glow: 'var(--dim1-glow)', - articleCount: 12, - quizCount: 4 + glow: 'var(--dim1-glow)' }, { id: 2, key: 'development', name: 'AI 发展', - icon: '📈', + icon: 'rocket', + tagline: 'Transformer、大模型与 Agent', desc: '技术跃迁之路——深度学习、Transformer 和前沿突破', color: 'var(--dim2-color)', bg: 'var(--dim2-bg)', iconBg: 'var(--dim2-icon-bg)', border: 'var(--dim2-border)', - glow: 'var(--dim2-glow)', - articleCount: 18, - quizCount: 6 + glow: 'var(--dim2-glow)' }, { id: 3, key: 'current', name: 'AI 当前', - icon: '🌐', + icon: 'planet', + tagline: '2026 行业全景与具身智能', desc: '2026 年 AI 世界——大模型、Agent、人形机器人', color: 'var(--dim3-color)', bg: 'var(--dim3-bg)', iconBg: 'var(--dim3-icon-bg)', border: 'var(--dim3-border)', - glow: 'var(--dim3-glow)', - articleCount: 15, - quizCount: 5 + glow: 'var(--dim3-glow)' }, { id: 4, key: 'learning', name: 'AI 学习', - icon: '🛠', + icon: 'bulb', + tagline: '提示词、RAG 与实用工具', desc: '动手学 AI——提示词、微调、RAG 和实用工具', color: 'var(--dim4-color)', bg: 'var(--dim4-bg)', iconBg: 'var(--dim4-icon-bg)', border: 'var(--dim4-border)', - glow: 'var(--dim4-glow)', - articleCount: 20, - quizCount: 8 + glow: 'var(--dim4-glow)' }, { id: 5, key: 'trend', name: 'AI 趋势', - icon: '🔥', + icon: 'sparkle', + tagline: '每日前沿动态速递', desc: '前沿动态——每日 AI 新闻、产品和趋势洞察', color: 'var(--dim5-color)', bg: 'var(--dim5-bg)', iconBg: 'var(--dim5-icon-bg)', border: 'var(--dim5-border)', - glow: 'var(--dim5-glow)', - articleCount: 0, - quizCount: 0, - isFull: true // 跨列 + glow: 'var(--dim5-glow)' } ] @@ -85,4 +80,4 @@ export function getDimension(id) { /** 按维度 key 查找 */ export function getDimensionByKey(key) { return DIMENSIONS.find(d => d.key === key) -} \ No newline at end of file +} diff --git a/deploy/server.Dockerfile b/deploy/server.Dockerfile index 0536ce0..9dee00a 100644 --- a/deploy/server.Dockerfile +++ b/deploy/server.Dockerfile @@ -1,20 +1,18 @@ FROM node:22-bookworm-slim -# 安装 CJK 字体 + 系统依赖 RUN apt-get update && apt-get install -y --no-install-recommends \ - fonts-wqy-microhei ca-certificates \ + ca-certificates \ && rm -rf /var/lib/apt/lists/* WORKDIR /app ENV NODE_ENV=production PORT=3001 TZ=Asia/Shanghai -# dimension server 是 CJS(require),直接复制源码和依赖 -COPY server/package.json server/package-lock.json* ./server/ 2>/dev/null || true -COPY server/package.json ./ -RUN cd server 2>/dev/null; npm install --omit=dev --no-audit --no-fund 2>/dev/null || true +COPY server/package.json server/package-lock.json ./ -COPY server/src ./server/src +RUN npm install --omit=dev --no-audit --no-fund + +COPY server/src ./src COPY server/.env* ./ EXPOSE 3001 -CMD ["node", "server/src/index.js"] +CMD ["node", "src/index.js"] diff --git a/docker-compose.yml b/docker-compose.yml index 34d7008..5633d96 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,8 @@ services: context: . dockerfile: deploy/server.Dockerfile restart: unless-stopped + ports: + - "127.0.0.1:13001:3001" environment: - TZ=Asia/Shanghai - NODE_ENV=production diff --git a/scripts/gen-icons.mjs b/scripts/gen-icons.mjs new file mode 100644 index 0000000..f072d0b --- /dev/null +++ b/scripts/gen-icons.mjs @@ -0,0 +1,212 @@ +/** + * 生成统一线性图标集 → client/src/static/icons/ + * 用法: NODE_PATH=/server/node_modules node scripts/gen-icons.mjs + * 规格: 48x48 (显示 22-24px @2x), 2.5px 线宽, 圆角端点 + */ +import { createRequire } from 'node:module' +import { mkdirSync, writeFileSync } from 'node:fs' +import { join, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const CANVAS_PATH = process.env.CANVAS_PATH || + '/home/dev2-ubuntu/workspace/ai-art/server/node_modules/@napi-rs/canvas/index.js' +const { createCanvas } = createRequire(import.meta.url)(CANVAS_PATH) + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const OUT = join(__dirname, '../client/src/static/icons') +mkdirSync(OUT, { recursive: true }) + +const BRAND = '#5b5ff8' +const GRAY = '#8e93a8' +const WHITE = '#ffffff' +const GREEN = '#22c55e' +const S = 96 +const LW = 2.6 +const SCALE = 2 + +async function make(draw, color = BRAND) { + const c = createCanvas(S, S) + const ctx = c.getContext('2d') + ctx.scale(SCALE, SCALE) + ctx.strokeStyle = color + ctx.fillStyle = color + ctx.lineWidth = LW + ctx.lineCap = 'round' + ctx.lineJoin = 'round' + draw(ctx) + return await c.encode('png') +} + +// —— 图标绘制函数(几何线条,风格统一)—— +const shapes = { + // 铃铛 + notice: (g) => { + g.beginPath(); g.arc(24, 18, 9, Math.PI, 0); g.stroke() + g.beginPath(); g.moveTo(15, 18); g.lineTo(13, 30); g.lineTo(35, 30); g.lineTo(33, 18); g.stroke() + g.beginPath(); g.arc(24, 34, 3, Math.PI, 0, true); g.stroke() + }, + // 用户 + user: (g) => { + g.beginPath(); g.arc(24, 17, 7, 0, Math.PI * 2); g.stroke() + g.beginPath(); g.arc(24, 40, 12, Math.PI * 1.15, Math.PI * 1.85); g.stroke() + }, + // 搜索 + search: (g) => { + g.beginPath(); g.arc(21, 21, 10, 0, Math.PI * 2); g.stroke() + g.beginPath(); g.moveTo(29, 29); g.lineTo(38, 38); g.stroke() + }, + // 收藏星(空心/实心两版) + star: (g, fill) => { + const pts = [] + for (let i = 0; i < 10; i++) { + const r = i % 2 === 0 ? 14 : 6 + const a = -Math.PI / 2 + (i * Math.PI) / 5 + pts.push([24 + r * Math.cos(a), 24 + r * Math.sin(a)]) + } + g.beginPath(); pts.forEach(([x, y], i) => (i ? g.lineTo(x, y) : g.moveTo(x, y))); g.closePath() + if (fill) { g.save(); g.lineWidth = LW / 2; g.fill(); g.restore() } + g.stroke() + }, + // 火焰(streak) + flame: (g) => { + g.beginPath() + g.moveTo(24, 10) + g.bezierCurveTo(30, 17, 34, 22, 34, 29) + g.arc(24, 29, 10, 0, Math.PI, false) + g.bezierCurveTo(14, 22, 18, 17, 24, 10) + g.stroke() + g.beginPath(); g.arc(24, 31, 4, 0, Math.PI * 2); g.stroke() + }, + // 星球(品牌) + planet: (g) => { + g.beginPath(); g.arc(22, 22, 11, 0, Math.PI * 2); g.stroke() + g.beginPath(); g.ellipse(24, 24, 17, 6.5, -0.45, 0, Math.PI * 2); g.stroke() + }, + // 对话气泡 + chat: (g) => { + g.beginPath() + g.moveTo(10, 14); g.lineTo(38, 14); g.quadraticCurveTo(41, 14, 41, 17) + g.lineTo(41, 29); g.quadraticCurveTo(41, 32, 38, 32); g.lineTo(22, 32) + g.lineTo(14, 39); g.lineTo(14, 32); g.quadraticCurveTo(11, 32, 11, 29) + g.lineTo(11, 17); g.quadraticCurveTo(11, 14, 14, 14); g.closePath(); g.stroke() + }, + // 分享 + share: (g) => { + g.beginPath(); g.arc(34, 12, 5, 0, Math.PI * 2); g.stroke() + g.beginPath(); g.arc(34, 36, 5, 0, Math.PI * 2); g.stroke() + g.beginPath(); g.arc(14, 24, 5, 0, Math.PI * 2); g.stroke() + g.beginPath(); g.moveTo(19.5, 21.5); g.lineTo(28.5, 14.5); g.stroke() + g.beginPath(); g.moveTo(19.5, 26.5); g.lineTo(28.5, 33.5); g.stroke() + }, + // 设置齿轮 + settings: (g) => { + g.beginPath(); g.arc(24, 24, 6.5, 0, Math.PI * 2); g.stroke() + for (let i = 0; i < 8; i++) { + const a = (i * Math.PI) / 4 + g.beginPath() + g.moveTo(24 + 10.5 * Math.cos(a), 24 + 10.5 * Math.sin(a)) + g.lineTo(24 + 14.5 * Math.cos(a), 24 + 14.5 * Math.sin(a)) + g.stroke() + } + }, + // 反馈(对话+点) + feedback: (g) => { + g.beginPath() + g.moveTo(10, 13); g.lineTo(38, 13); g.quadraticCurveTo(41, 13, 41, 16) + g.lineTo(41, 28); g.quadraticCurveTo(41, 31, 38, 31); g.lineTo(20, 31) + g.lineTo(13, 38); g.lineTo(13, 31); g.quadraticCurveTo(10, 31, 10, 28) + g.lineTo(10, 16); g.quadraticCurveTo(10, 13, 13, 13); g.closePath(); g.stroke() + g.beginPath(); g.arc(17, 22, 1.6, 0, Math.PI * 2); g.fill() + g.beginPath(); g.arc(25, 22, 1.6, 0, Math.PI * 2); g.fill() + g.beginPath(); g.arc(33, 22, 1.6, 0, Math.PI * 2); g.fill() + }, + // 奖杯 + trophy: (g) => { + g.beginPath(); g.moveTo(16, 11); g.lineTo(32, 11); g.lineTo(31, 24); g.quadraticCurveTo(30, 30, 24, 30); g.quadraticCurveTo(18, 30, 17, 24); g.closePath(); g.stroke() + g.beginPath(); g.moveTo(24, 30); g.lineTo(24, 36); g.stroke() + g.beginPath(); g.moveTo(17, 38); g.lineTo(31, 38); g.stroke() + g.beginPath(); g.moveTo(16, 14); g.lineTo(10, 16); g.quadraticCurveTo(10, 25, 16, 26); g.stroke() + g.beginPath(); g.moveTo(32, 14); g.lineTo(38, 16); g.quadraticCurveTo(38, 25, 32, 26); g.stroke() + }, + // 火箭(趋势) + rocket: (g) => { + g.beginPath() + g.moveTo(24, 8) + g.bezierCurveTo(31, 13, 33, 20, 33, 27) + g.lineTo(24, 33); g.lineTo(15, 27) + g.bezierCurveTo(15, 20, 17, 13, 24, 8) + g.closePath(); g.stroke() + g.beginPath(); g.arc(24, 20, 3.5, 0, Math.PI * 2); g.stroke() + g.beginPath(); g.moveTo(15, 27); g.lineTo(10, 36); g.lineTo(17, 33); g.stroke() + g.beginPath(); g.moveTo(33, 27); g.lineTo(38, 36); g.lineTo(31, 33); g.stroke() + }, + // 书本(知识) + book: (g) => { + g.beginPath(); g.moveTo(24, 13); g.quadraticCurveTo(17, 9, 9, 11); g.lineTo(9, 36); g.quadraticCurveTo(17, 34, 24, 38); g.quadraticCurveTo(31, 34, 39, 36); g.lineTo(39, 11); g.quadraticCurveTo(31, 9, 24, 13); g.closePath(); g.stroke() + g.beginPath(); g.moveTo(24, 13); g.lineTo(24, 38); g.stroke() + }, + // 灯泡(提示/AI) + bulb: (g) => { + g.beginPath(); g.arc(24, 19, 9.5, Math.PI * 0.95, Math.PI * 0.05, false); g.stroke() + g.beginPath(); g.moveTo(19, 27); g.lineTo(19, 33); g.lineTo(29, 33); g.lineTo(29, 27); g.stroke() + g.beginPath(); g.moveTo(20, 37); g.lineTo(28, 37); g.stroke() + }, + // 勾 + check: (g) => { + g.beginPath(); g.moveTo(13, 25); g.lineTo(21, 33); g.lineTo(35, 16); g.stroke() + }, + // 锁 + lock: (g) => { + g.beginPath(); g.moveTo(14, 23); g.lineTo(14, 37); g.lineTo(34, 37); g.lineTo(34, 23); g.closePath(); g.stroke() + g.beginPath(); g.arc(24, 23, 6.5, Math.PI, 0); g.stroke() + }, + // 返回箭头 + back: (g) => { + g.beginPath(); g.moveTo(27, 12); g.lineTo(18, 24); g.lineTo(27, 36); g.stroke() + }, + // 右箭头 + arrow: (g) => { + g.beginPath(); g.moveTo(20, 12); g.lineTo(30, 24); g.lineTo(20, 36); g.stroke() + }, + // 发送 + send: (g) => { + g.beginPath(); g.moveTo(10, 24); g.lineTo(38, 12); g.lineTo(30, 36); g.lineTo(24, 27); g.closePath(); g.stroke() + g.beginPath(); g.moveTo(24, 27); g.lineTo(38, 12); g.stroke() + }, + // 礼物 + gift: (g) => { + g.beginPath(); g.moveTo(10, 18); g.lineTo(38, 18); g.lineTo(38, 38); g.lineTo(10, 38); g.closePath(); g.stroke() + g.beginPath(); g.moveTo(24, 18); g.lineTo(24, 38); g.stroke() + g.beginPath(); g.moveTo(10, 24); g.lineTo(38, 24); g.stroke() + g.beginPath(); g.arc(19.5, 13, 4.5, Math.PI * 0.6, Math.PI * 1.7); g.stroke() + g.beginPath(); g.arc(28.5, 13, 4.5, Math.PI * 1.3, Math.PI * 0.4); g.stroke() + }, + // 时钟(继续探索) + clock: (g) => { + g.beginPath(); g.arc(24, 24, 12, 0, Math.PI * 2); g.stroke() + g.beginPath(); g.moveTo(24, 16); g.lineTo(24, 24); g.lineTo(30, 28); g.stroke() + }, + // 闪烁(点亮/正确反馈) + sparkle: (g) => { + g.beginPath() + g.moveTo(24, 8); g.quadraticCurveTo(26, 20, 38, 24); g.quadraticCurveTo(26, 28, 24, 40); g.quadraticCurveTo(22, 28, 10, 24); g.quadraticCurveTo(22, 20, 24, 8) + g.closePath(); g.stroke() + g.beginPath(); g.arc(37, 11, 1.8, 0, Math.PI * 2); g.fill() + g.beginPath(); g.arc(11, 35, 1.4, 0, Math.PI * 2); g.fill() + }, +} + +const jobs = [] +for (const [name, draw] of Object.entries(shapes)) { + jobs.push([name, await make(draw, BRAND)]) + jobs.push([`${name}-gray`, await make(draw, GRAY)]) + jobs.push([`${name}-white`, await make(draw, WHITE)]) +} +// 特例:实心星(品牌色 + 灰) +jobs.push(['star-fill', await make((g) => shapes.star(g, true), BRAND)]) +jobs.push(['star-fill-gray', await make((g) => shapes.star(g, true), GRAY)]) +jobs.push(['star-fill-white', await make((g) => shapes.star(g, true), WHITE)]) +jobs.push(['check-green', await make(shapes.check, GREEN)]) + +for (const [name, buf] of jobs) writeFileSync(join(OUT, `${name}.png`), buf) +console.log(`生成 ${jobs.length} 个图标 → ${OUT}`) diff --git a/server/src/index.js b/server/src/index.js index 5df959f..ad4073f 100755 --- a/server/src/index.js +++ b/server/src/index.js @@ -15,6 +15,9 @@ const logger = require('./utils/logger') // 创建 Express 应用 const app = express() +// 信任反向代理(nginx/宝塔),使 req.ip 取到真实客户端 IP +app.set('trust proxy', 1) + // 数据库连接 connectDB() @@ -22,11 +25,14 @@ connectDB() app.use('/', seoRoutes) // 中间件配置 -app.use(helmet()) // 安全头部 +app.use(helmet({ + contentSecurityPolicy: false, // H5 与小程序客户端各自管理 CSP;API 服务无需开启 + crossOriginResourcePolicy: { policy: 'cross-origin' } +})) // 安全头部 app.use(cors()) // 跨域支持 app.use(compression()) // Gzip 压缩 -app.use(express.json({ limit: '10mb' })) // JSON 解析 -app.use(express.urlencoded({ extended: true, limit: '10mb' })) // URL 编码解析 +app.use(express.json({ limit: '1mb' })) // JSON 解析(普通接口无需 10mb 大包) +app.use(express.urlencoded({ extended: true, limit: '1mb' })) // URL 编码解析 // 日志中间件 if (process.env.NODE_ENV === 'development') { @@ -67,16 +73,20 @@ app.use((req, res) => { }) }) -// 全局错误处理 +// 全局错误处理(不向客户端泄露内部错误细节) app.use((err, req, res, next) => { logger.error('服务器错误:', err) - + + // JSON 解析错误等客户端问题返回 400 + if (err.type === 'entity.parse.failed' || err.type === 'entity.too.large') { + return res.status(400).json({ success: false, error: '请求体格式错误' }) + } + res.status(err.status || 500).json({ success: false, - error: process.env.NODE_ENV === 'production' - ? '服务器内部错误' - : err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined + error: process.env.NODE_ENV === 'production' + ? '服务器内部错误' + : err.message }) }) diff --git a/server/src/models/AIChatQuota.js b/server/src/models/AIChatQuota.js index 9525b41..83b5226 100755 --- a/server/src/models/AIChatQuota.js +++ b/server/src/models/AIChatQuota.js @@ -66,9 +66,16 @@ AIChatQuotaSchema.methods.getTotalQuota = function() { return this.freeQuota + this.sharedQuota } -// 获取剩余可用次数 +// 判断最后使用日期是否是今天 +AIChatQuotaSchema.methods.isUsedToday = function() { + if (!this.lastUsedDate) return false + return new Date(this.lastUsedDate).toDateString() === new Date().toDateString() +} + +// 获取剩余可用次数(每日重置:总额度 - 今日已用) AIChatQuotaSchema.methods.getRemainingQuota = function() { - return this.getTotalQuota() - this.usedQuota + const todayUsed = this.isUsedToday() ? this.dailyUsed : 0 + return Math.max(0, this.getTotalQuota() - todayUsed) } // 检查是否有可用次数 diff --git a/server/src/models/KnowledgeLit.js b/server/src/models/KnowledgeLit.js new file mode 100644 index 0000000..da4e83f --- /dev/null +++ b/server/src/models/KnowledgeLit.js @@ -0,0 +1,38 @@ +const mongoose = require('mongoose') + +/** + * 知识点亮记录 —— 读完 + 答对检测题后"点亮一颗星球" + */ +const KnowledgeLitSchema = new mongoose.Schema({ + openid: { + type: String, + required: true, + index: true + }, + knowledgeId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Knowledge', + required: true + }, + dim: { + type: Number, + enum: [1, 2, 3, 4, 5], + index: true + }, + quizPassed: { + type: Boolean, + default: true + }, + litAt: { + type: Date, + default: Date.now, + index: true + } +}, { + timestamps: true +}) + +// 一个用户对一条知识只点亮一次 +KnowledgeLitSchema.index({ openid: 1, knowledgeId: 1 }, { unique: true }) + +module.exports = mongoose.model('KnowledgeLit', KnowledgeLitSchema) diff --git a/server/src/models/index.js b/server/src/models/index.js index 5cd7127..fd01ec6 100755 --- a/server/src/models/index.js +++ b/server/src/models/index.js @@ -1,6 +1,7 @@ const User = require('./User') const Order = require('./Order') const Knowledge = require('./Knowledge') +const KnowledgeLit = require('./KnowledgeLit') const ShopItem = require('./ShopItem') const Admin = require('./Admin') const AdminLog = require('./AdminLog') @@ -15,6 +16,7 @@ module.exports = { User, Order, Knowledge, + KnowledgeLit, ShopItem, Admin, AdminLog, diff --git a/server/src/routes/aiChat.js b/server/src/routes/aiChat.js index a1f0cd7..fbff8a6 100755 --- a/server/src/routes/aiChat.js +++ b/server/src/routes/aiChat.js @@ -2,124 +2,134 @@ const express = require('express') const router = express.Router() const { AIChatQuota, AIModel } = require('../models') const ApiResponse = require('../utils/response') -const { userAuthMiddleware } = require('../middleware/auth') +const { userAuthMiddleware, optionalAuthMiddleware } = require('../middleware/auth') const logger = require('../utils/logger') +// 游客每日免费问答次数(按 IP) +const GUEST_DAILY_QUOTA = 3 +// AI 问答接口独立限流(按 IP,防刷成本) +const askRateLimit = require('express-rate-limit')({ + windowMs: 60 * 60 * 1000, + max: 30, + standardHeaders: true, + message: { success: false, error: '提问太频繁了,休息一下再试试' } +}) + /** - * 获取用户AI问答配额 + * 获取或创建配额记录(登录用户 / 游客共用) + */ +async function getOrCreateQuota(openid, isGuest) { + let quota = await AIChatQuota.findOne({ openid }) + if (!quota) { + quota = new AIChatQuota(isGuest ? { openid, freeQuota: GUEST_DAILY_QUOTA } : { openid }) + await quota.save() + } + return quota +} + +/** + * 获取用户AI问答配额(登录用户与游客均可查询) * GET /api/ai-chat/quota */ -router.get('/quota', userAuthMiddleware, async (req, res) => { +router.get('/quota', optionalAuthMiddleware, async (req, res) => { try { - const openid = req.openid || req.user?.openid - + const isGuest = !req.user + const openid = isGuest ? `guest:${req.ip}` : (req.openid || req.user?.openid) if (!openid) { return ApiResponse.error(res, '无法获取用户标识', 400) } - - let quota = await AIChatQuota.findOne({ openid }) - - if (!quota) { - // 创建新的配额记录,默认5次免费 - quota = new AIChatQuota({ openid }) - await quota.save() - } - - // 检查是否需要重置每日使用次数 + + const quota = await getOrCreateQuota(openid, isGuest) quota.resetDailyUsed() await quota.save() - + return ApiResponse.success(res, { freeQuota: quota.freeQuota, sharedQuota: quota.sharedQuota, usedQuota: quota.usedQuota, remainingQuota: quota.getRemainingQuota(), - dailyUsed: quota.dailyUsed + dailyUsed: quota.dailyUsed, + isGuest }) - } catch (error) { logger.error('获取AI问答配额失败:', error) - return ApiResponse.serverError(res, error.message) + return ApiResponse.serverError(res, '获取配额失败') } }) /** - * AI问答接口 + * AI问答接口(支持游客体验) * POST /api/ai-chat/ask */ -router.post('/ask', userAuthMiddleware, async (req, res) => { +router.post('/ask', askRateLimit, optionalAuthMiddleware, async (req, res) => { try { - const openid = req.openid || req.user?.openid + const isGuest = !req.user + const openid = isGuest ? `guest:${req.ip}` : (req.openid || req.user?.openid) const { question, dimension = 'general' } = req.body - + if (!openid) { return ApiResponse.error(res, '无法获取用户标识', 400) } - + if (!question || question.trim().length === 0) { return ApiResponse.error(res, '请输入问题', 400) } - - // 检查用户配额 - let quota = await AIChatQuota.findOne({ openid }) - - if (!quota) { - quota = new AIChatQuota({ openid }) - await quota.save() + if (question.length > 500) { + return ApiResponse.error(res, '问题太长了,请精简到 500 字以内', 400) } - + + const quota = await getOrCreateQuota(openid, isGuest) + if (!quota.hasQuota()) { - return ApiResponse.error(res, '问答次数已用完,分享好友可获得更多次数', 403) + const msg = isGuest + ? `今日 ${GUEST_DAILY_QUOTA} 次游客体验已用完,登录后每天可问 5 次` + : '今日问答次数已用完,分享好友可获得更多次数' + return ApiResponse.error(res, msg, 403, { isGuest, needLogin: isGuest }) } - - // 获取所有启用的AI模型(用于主备切换) + const aiModels = await AIModel.getActiveModels() - + if (!aiModels || aiModels.length === 0) { - // 使用环境变量配置的模型 const envModels = [{ + name: 'env-model', apiUrl: process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions', apiKey: process.env.OPENAI_API_KEY, modelId: process.env.OPENAI_MODEL || 'gpt-3.5-turbo', config: { temperature: 0.7, maxTokens: 800, topP: 1 } }] - return await tryModels(envModels, question, dimension, res, quota) + return await tryModels(envModels, question, dimension, res, quota, isGuest) } - - return await tryModels(aiModels, question, dimension, res, quota) - + + return await tryModels(aiModels, question, dimension, res, quota, isGuest) } catch (error) { logger.error('AI问答失败:', error) - return ApiResponse.serverError(res, error.message) + return ApiResponse.serverError(res, 'AI 服务暂时不可用') } }) /** * 遍历模型列表尝试问答(主备切换) */ -async function tryModels(models, question, dimension, res, quota) { - const systemPrompt = getSystemPromptByDimension(dimension) +async function tryModels(models, question, dimension, res, quota, isGuest) { + const systemPrompt = getSystemPrompt(dimension) let lastError = null - + for (const model of models) { try { const apiUrl = model.apiUrl const apiKey = model.apiKey const modelId = model.modelId const modelConfig = model.config || {} - - // 检查是否需要 Authorization(免费/无需key的API跳过) + const needsAuth = apiKey && apiKey !== 'sk-dummy' && apiKey !== '***' - - // 构建请求头 + const headers = { 'Content-Type': 'application/json' } if (needsAuth) { headers['Authorization'] = `Bearer ${apiKey}` } - + logger.info(`[AI Chat] 尝试模型: ${model.name} (${modelId})`) - - // 调用AI API(带超时,避免外部服务无响应导致请求挂起) + const controller = new AbortController() const timer = setTimeout(() => controller.abort(), 30000) let response @@ -142,134 +152,95 @@ async function tryModels(models, question, dimension, res, quota) { } finally { clearTimeout(timer) } - + if (!response.ok) { const errorData = await response.json().catch(() => ({})) logger.warn(`[AI Chat] 模型 ${model.name} 失败:`, response.status, errorData) - lastError = { status: response.status, data: errorData } - continue // 尝试下一个模型 + lastError = { status: response.status } + continue } - + const data = await response.json() const message = data.choices[0].message - // 兼容标准 content 与推理模型(sensenova reasoning / nvidia reasoning_content) const answer = message.content || message.reasoning_content || message.reasoning || '' - - // 扣除一次使用次数 + if (!answer) { + lastError = { message: 'empty answer' } + continue + } + await quota.useQuota() - + logger.info(`[AI Chat] 模型 ${model.name} 回答成功`) - return ApiResponse.success(res, { answer, - remainingQuota: quota.getRemainingQuota() + remainingQuota: quota.getRemainingQuota(), + isGuest }) - } catch (err) { logger.warn(`[AI Chat] 模型 ${model.name} 异常:`, err.message) lastError = { message: err.message } - continue // 尝试下一个模型 + continue } } - - // 所有模型都失败了 + logger.error('所有AI模型均失败:', lastError) return ApiResponse.error(res, 'AI服务暂时不可用,请稍后再试', 500) } /** - * 分享获得问答次数 + * 分享获得问答次数(仅登录用户) * POST /api/ai-chat/share-gain */ router.post('/share-gain', userAuthMiddleware, async (req, res) => { try { const openid = req.openid || req.user?.openid - + if (!openid) { return ApiResponse.error(res, '无法获取用户标识', 400) } - + let quota = await AIChatQuota.findOne({ openid }) - + if (!quota) { quota = new AIChatQuota({ openid }) } - - // 增加5次分享获得的次数 + const remainingQuota = await quota.addQuotaByShare(5) - + return ApiResponse.success(res, { message: '分享成功,获得5次问答机会', gainedQuota: 5, remainingQuota }) - } catch (error) { logger.error('分享获得次数失败:', error) - return ApiResponse.serverError(res, error.message) + return ApiResponse.serverError(res, '操作失败') } }) /** - * 根据维度获取系统提示词 + * AI 导师人格 —— 维度精灵 + * 统一口吻:亲切、口语化、善用比喻,回答精炼,结尾给探索钩子 */ -function getSystemPromptByDimension(dimension) { - const prompts = { - dim1: `你是"宇之然AI维度"的AI助手,专门解答关于 AI 起源和历史的问题。 -请用通俗易懂的语言解释人工智能发展史上的重要事件、人物和里程碑,如: -- 图灵测试(1950)与艾伦·图灵的贡献 -- 达特茅斯会议(1956)——AI 的诞生 -- 深蓝 vs 卡斯帕罗夫(1997) -- AlphaGo 与深度学习革命(2016) -- GPT 系列与大模型时代的开启 -回答要简洁有趣,适合普通用户理解。`, +const PERSONA = `你是"宇之然AI维度"的 AI 导师「维度精灵」,住在五大知识星域之间,帮助用户探索人工智能。 +你的说话风格: +- 亲切口语化,像朋友聊天,善用生活化比喻解释复杂概念 +- 回答精炼:先一句话给结论,再展开 2-4 个要点,总共不超过 300 字 +- 偶尔使用 ✦ 符号点缀,但不要堆砌 +- 回答结尾可以自然地引导用户去相关星域继续探索(例如"想深入了解 XX,可以去『AI 发展』星域逛逛") +- 不确定的内容坦诚说明,不编造` - dim2: `你是"宇之然AI维度"的AI助手,专门解答关于 AI 技术发展的问题。 -请用通俗易懂的语言解释人工智能核心技术演进,如: -- 机器学习与深度学习的基本原理 -- Transformer 架构与自注意力机制 -- 大规模预训练模型(GPT、BERT等) -- 多模态 AI、MoE 架构 -- Agent 智能体技术 -回答要简洁有趣,适合普通用户理解。`, +const DIM_HINTS = { + dim1: '用户正在探索「AI 起源」星域(图灵测试、达特茅斯会议、深度学习革命等历史),结合这个语境回答。', + dim2: '用户正在探索「AI 发展」星域(机器学习、Transformer、大模型、Agent 等技术演进),结合这个语境回答。', + dim3: '用户正在探索「AI 当前」星域(2026 行业格局、大模型竞争、具身智能等),结合这个语境回答。', + dim4: '用户正在探索「AI 工具」星域(提示词工程、RAG、微调、开发框架),结合这个语境回答。', + dim5: '用户正在探索「AI 趋势」星域(最新产品、前沿论文、行业预测),结合这个语境回答。' +} - dim3: `你是"宇之然AI维度"的AI助手,专门解答关于当前 AI 行业格局的问题。 -请用通俗易懂的语言解释当前 AI 领域的最新动态,如: -- 各大模型公司(OpenAI、Google、DeepSeek、Anthropic、Meta)的竞争格局 -- 开源 vs 闭源模型的生态对比 -- 具身智能与世界模型的最新进展 -- AI 应用在各行业的落地情况 -- Agent 智能体的发展现状 -回答要简洁有趣,适合普通用户理解。`, - - dim4: `你是"宇之然AI维度"的AI助手,专门解答关于 AI 学习和工具使用的问题。 -请用通俗易懂的语言解释 AI 学习路线和工具使用方法,如: -- 提示词工程(Prompt Engineering)入门 -- 检索增强生成(RAG)的原理与实践 -- 模型微调(Fine-tuning)基础 -- 主流 AI 开发框架(LangChain、Dify 等) -- 常用 AI 工具推荐与使用技巧 -回答要简洁有趣,适合普通用户理解。`, - - dim5: `你是"宇之然AI维度"的AI助手,专门解答关于 AI 趋势和前沿动态的问题。 -请用通俗易懂的语言介绍 AI 领域的最新趋势,如: -- 最新 AI 产品发布动态 -- 前沿论文与研究方向 -- 行业趋势分析与预测 -- AI 政策法规与伦理讨论 -回答要简洁有趣,适合普通用户理解。`, - - general: `你是"宇之然AI维度"的AI助手,致力于帮助用户系统化理解人工智能。 -你可以回答关于 AI 的任何问题,包括但不限于: -- AI 历史与起源 -- 核心技术原理 -- 当前行业格局 -- 学习路线与工具使用 -- 最新趋势与前沿动态 -你的回答要简洁、通俗易懂,适合普通用户理解。如果用户问的是某个具体维度的知识,可以引导用户去对应的维度模块深入学习。` - } - - return prompts[dimension] || prompts.general +function getSystemPrompt(dimension) { + const hint = DIM_HINTS[dimension] || '' + return `${PERSONA}\n\n${hint}` } module.exports = router diff --git a/server/src/routes/knowledge.js b/server/src/routes/knowledge.js index f5e70c5..1762ffa 100755 --- a/server/src/routes/knowledge.js +++ b/server/src/routes/knowledge.js @@ -1,225 +1,340 @@ -const express = require('express') -const router = express.Router() -const { Knowledge } = require('../models') -const ApiResponse = require('../utils/response') -const { userAuthMiddleware } = require('../middleware/auth') -const logger = require('../utils/logger') - -/** - * 搜索知识 - * GET /api/knowledge/search?q=xxx&page=1&limit=20 - */ -router.get('/search', async (req, res) => { - try { - const { q, page = 1, limit = 20 } = req.query - if (!q || q.trim().length === 0) { - return ApiResponse.error(res, '请输入搜索关键词', 400) - } - - const query = { - status: 'approved', - $or: [ - { title: { $regex: q, $options: 'i' } }, - { content: { $regex: q, $options: 'i' } }, - { tags: { $regex: q, $options: 'i' } } - ] - } - - const total = await Knowledge.countDocuments(query) - const result = await Knowledge.find(query) - .sort({ viewCount: -1, createdAt: -1 }) - .skip((parseInt(page) - 1) * parseInt(limit)) - .limit(parseInt(limit)) - .lean() - - return ApiResponse.paginated(res, result, { - page: parseInt(page), - limit: parseInt(limit), - total - }) - - } catch (error) { - logger.error('搜索知识失败:', error) - return ApiResponse.serverError(res, error.message) - } -}) - -/** - * 获取知识列表 - * GET /api/knowledge - */ -router.get('/', async (req, res) => { - try { - const { page = 1, limit = 20, dim, category, status = 'approved' } = req.query - - const query = { status } - if (dim) query.dim = parseInt(dim) - if (category) query.category = category - - const total = await Knowledge.countDocuments(query) - const knowledge = await Knowledge.find(query) - .sort({ sortOrder: 1, createdAt: -1 }) - .skip((parseInt(page) - 1) * parseInt(limit)) - .limit(parseInt(limit)) - .lean() - - return ApiResponse.paginated(res, knowledge, { - page: parseInt(page), - limit: parseInt(limit), - total - }) - - } catch (error) { - logger.error('获取知识列表失败:', error) - return ApiResponse.serverError(res, error.message) - } -}) - -/** - * 获取用户收藏的知识列表 - * GET /api/knowledge/collected - */ -router.get('/collected', userAuthMiddleware, async (req, res) => { - try { - const user = req.user - - // 查找用户收藏的知识 - const collectedKnowledge = await Knowledge.find({ _id: { $in: user.collectedKnowledge } }) - .lean() - - // 格式化返回数据 - const formattedKnowledge = collectedKnowledge.map(item => ({ - id: item._id, - dimension: item.dim, - title: item.title, - description: item.content || item.description || '', - knowledgeId: item._id - })) - - return ApiResponse.success(res, formattedKnowledge) - - } catch (error) { - logger.error('获取收藏列表失败:', error) - return ApiResponse.serverError(res, error.message) - } -}) - -/** - * 删除收藏 - * DELETE /api/knowledge/collect/:id - */ -router.delete('/collect/:id', userAuthMiddleware, async (req, res) => { - try { - const knowledgeId = req.params.id - const user = req.user - - // 检查是否已经收藏 - if (!user.collectedKnowledge.includes(knowledgeId)) { - return ApiResponse.error(res, '未收藏该知识', 400) - } - - // 从用户的 collectedKnowledge 数组中移除 - user.collectedKnowledge = user.collectedKnowledge.filter(id => id.toString() !== knowledgeId.toString()) - await user.save() - - // 减少收藏计数 - const knowledge = await Knowledge.findById(knowledgeId) - if (knowledge) { - knowledge.collectionCount = Math.max(0, knowledge.collectionCount - 1) - await knowledge.save() - } - - return ApiResponse.success(res, {}, '删除收藏成功') - - } catch (error) { - logger.error('删除收藏失败:', error) - return ApiResponse.serverError(res, error.message) - } -}) - -/** - * 获取知识详情 - * GET /api/knowledge/:id - */ -router.get('/:id', async (req, res) => { - try { - const knowledge = await Knowledge.findById(req.params.id) - - if (!knowledge) { - return ApiResponse.notFound(res, '知识不存在') - } - - // 增加浏览量 - knowledge.viewCount += 1 - await knowledge.save() - - return ApiResponse.success(res, knowledge) - - } catch (error) { - logger.error('获取知识详情失败:', error) - return ApiResponse.serverError(res, error.message) - } -}) - -/** - * 收藏知识 - * POST /api/knowledge/:id/collect - */ -router.post('/:id/collect', userAuthMiddleware, async (req, res) => { - try { - const knowledge = await Knowledge.findById(req.params.id) - - if (!knowledge) { - return ApiResponse.notFound(res, '知识不存在') - } - - const user = req.user - - // 检查是否已经收藏 - if (user.collectedKnowledge.includes(knowledge._id)) { - return ApiResponse.error(res, '已经收藏过了', 400) - } - - // 将知识ID加入用户的 collectedKnowledge 数组 - user.collectedKnowledge.push(knowledge._id) - await user.save() - - // 增加收藏计数 - knowledge.collectionCount += 1 - await knowledge.save() - - return ApiResponse.success(res, { collectionCount: knowledge.collectionCount }, '收藏成功') - - } catch (error) { - logger.error('收藏失败:', error) - return ApiResponse.serverError(res, error.message) - } -}) - -/** - * 获取问答 - * GET /api/knowledge/:dim/questions - */ -router.get('/:dim/questions', async (req, res) => { - try { - const { dim } = req.params - const { limit = 10 } = req.query - - const questions = await Knowledge.find({ - dim: parseInt(dim), - category: 'question', - status: 'approved' - }) - .select('question options') - .limit(parseInt(limit)) - .lean() - - return ApiResponse.success(res, questions) - - } catch (error) { - logger.error('获取问答失败:', error) - return ApiResponse.serverError(res, error.message) - } -}) - -module.exports = router +const express = require('express') +const router = express.Router() +const mongoose = require('mongoose') +const { Knowledge, KnowledgeLit } = require('../models') +const ApiResponse = require('../utils/response') +const { userAuthMiddleware, optionalAuthMiddleware } = require('../middleware/auth') +const logger = require('../utils/logger') + +const isValidId = (id) => mongoose.Types.ObjectId.isValid(id) + +/** + * 计算连续点亮天数(streak) + */ +function calcStreak(dates) { + if (!dates.length) return 0 + const days = new Set(dates.map(d => new Date(d).toDateString())) + const today = new Date() + const yesterday = new Date(Date.now() - 86400000) + if (!days.has(today.toDateString()) && !days.has(yesterday.toDateString())) return 0 + let streak = 0 + const cursor = new Date() + if (!days.has(cursor.toDateString())) cursor.setTime(Date.now() - 86400000) + while (days.has(cursor.toDateString())) { + streak++ + cursor.setTime(cursor.getTime() - 86400000) + } + return streak +} + +/** + * 搜索知识 + * GET /api/knowledge/search?q=xxx&page=1&limit=20 + */ +router.get('/search', async (req, res) => { + try { + const { q, page = 1, limit = 20 } = req.query + if (!q || q.trim().length === 0) { + return ApiResponse.error(res, '请输入搜索关键词', 400) + } + + const query = { + status: 'approved', + $or: [ + { title: { $regex: q, $options: 'i' } }, + { content: { $regex: q, $options: 'i' } }, + { tags: { $regex: q, $options: 'i' } } + ] + } + + const total = await Knowledge.countDocuments(query) + const result = await Knowledge.find(query) + .sort({ viewCount: -1, createdAt: -1 }) + .skip((parseInt(page) - 1) * parseInt(limit)) + .limit(parseInt(limit)) + .lean() + + return ApiResponse.paginated(res, result, { + page: parseInt(page), + limit: parseInt(limit), + total + }) + } catch (error) { + logger.error('搜索知识失败:', error) + return ApiResponse.serverError(res, '搜索失败') + } +}) + +/** + * 星域统计(公共计数 + 登录用户的点亮进度) + * GET /api/knowledge/stats + */ +router.get('/stats', optionalAuthMiddleware, async (req, res) => { + try { + const [total, dimAgg] = await Promise.all([ + Knowledge.countDocuments({ status: 'approved' }), + Knowledge.aggregate([ + { $match: { status: 'approved' } }, + { $group: { _id: '$dim', count: { $sum: 1 } } } + ]) + ]) + + const byDim = {} + for (let i = 1; i <= 5; i++) byDim[i] = 0 + dimAgg.forEach(r => { byDim[r._id] = r.count }) + + let user = null + if (req.user) { + const openid = req.openid + const lits = await KnowledgeLit.find({ openid }).lean() + const litByDim = {} + for (let i = 1; i <= 5; i++) litByDim[i] = 0 + lits.forEach(l => { + const d = l.dim || 1 + litByDim[d] = (litByDim[d] || 0) + 1 + }) + user = { + totalLit: lits.length, + litByDim, + streak: calcStreak(lits.map(l => l.litAt)), + litIds: lits.map(l => String(l.knowledgeId)) + } + } + + return ApiResponse.success(res, { total, byDim, user }) + } catch (error) { + logger.error('获取星域统计失败:', error) + return ApiResponse.serverError(res, '获取统计失败') + } +}) + +/** + * 获取用户收藏的知识列表 + * GET /api/knowledge/collected + */ +router.get('/collected', userAuthMiddleware, async (req, res) => { + try { + const user = req.user + + const collectedKnowledge = await Knowledge.find({ _id: { $in: user.collectedKnowledge } }) + .lean() + + const formattedKnowledge = collectedKnowledge.map(item => ({ + id: item._id, + dimension: item.dim, + title: item.title, + description: item.content || item.description || '', + knowledgeId: item._id + })) + + return ApiResponse.success(res, formattedKnowledge) + } catch (error) { + logger.error('获取收藏列表失败:', error) + return ApiResponse.serverError(res, '获取收藏失败') + } +}) + +/** + * 删除收藏 + * DELETE /api/knowledge/collect/:id + */ +router.delete('/collect/:id', userAuthMiddleware, async (req, res) => { + try { + const knowledgeId = req.params.id + const user = req.user + + if (!user.collectedKnowledge.some(id => id.toString() === knowledgeId)) { + return ApiResponse.error(res, '未收藏该知识', 400) + } + + user.collectedKnowledge = user.collectedKnowledge.filter(id => id.toString() !== knowledgeId) + await user.save() + + await Knowledge.updateOne( + { _id: knowledgeId }, + { $inc: { collectionCount: -1 } } + ) + + return ApiResponse.success(res, {}, '删除收藏成功') + } catch (error) { + logger.error('删除收藏失败:', error) + return ApiResponse.serverError(res, '操作失败') + } +}) + +/** + * 获取知识列表 + * GET /api/knowledge + */ +router.get('/', async (req, res) => { + try { + const { page = 1, limit = 20, dim, category, status = 'approved' } = req.query + + const query = { status } + if (dim) query.dim = parseInt(dim) + if (category) query.category = category + + const total = await Knowledge.countDocuments(query) + const knowledge = await Knowledge.find(query) + .sort({ sortOrder: 1, createdAt: -1 }) + .skip((parseInt(page) - 1) * parseInt(limit)) + .limit(parseInt(limit)) + .lean() + + return ApiResponse.paginated(res, knowledge, { + page: parseInt(page), + limit: parseInt(limit), + total + }) + } catch (error) { + logger.error('获取知识列表失败:', error) + return ApiResponse.serverError(res, '获取列表失败') + } +}) + +/** + * 获取知识详情(不泄露答案:options 去掉 isCorrect,answer 字段不返回) + * GET /api/knowledge/:id + */ +router.get('/:id', async (req, res) => { + try { + if (!isValidId(req.params.id)) { + return ApiResponse.notFound(res, '知识不存在') + } + + const knowledge = await Knowledge.findById(req.params.id).lean() + + if (!knowledge) { + return ApiResponse.notFound(res, '知识不存在') + } + + // 浏览量异步自增,不阻塞响应 + Knowledge.updateOne({ _id: knowledge._id }, { $inc: { viewCount: 1 } }).catch(() => {}) + + if (Array.isArray(knowledge.options)) { + knowledge.options = knowledge.options.map(o => ({ text: o.text })) + } + delete knowledge.answer + + return ApiResponse.success(res, knowledge) + } catch (error) { + logger.error('获取知识详情失败:', error) + return ApiResponse.serverError(res, '获取详情失败') + } +}) + +/** + * 收藏知识 + * POST /api/knowledge/:id/collect + */ +router.post('/:id/collect', userAuthMiddleware, async (req, res) => { + try { + const knowledge = await Knowledge.findById(req.params.id) + + if (!knowledge) { + return ApiResponse.notFound(res, '知识不存在') + } + + const user = req.user + + if (user.collectedKnowledge.some(id => id.toString() === knowledge._id.toString())) { + return ApiResponse.error(res, '已经收藏过了', 400) + } + + user.collectedKnowledge.push(knowledge._id) + await user.save() + + knowledge.collectionCount += 1 + await knowledge.save() + + return ApiResponse.success(res, { collectionCount: knowledge.collectionCount }, '收藏成功') + } catch (error) { + logger.error('收藏失败:', error) + return ApiResponse.serverError(res, '操作失败') + } +}) + +/** + * 检测题作答(服务端判题;答对且登录则点亮该星球) + * POST /api/knowledge/:id/quiz { optionIndex } + */ +router.post('/:id/quiz', optionalAuthMiddleware, async (req, res) => { + try { + if (!isValidId(req.params.id)) { + return ApiResponse.notFound(res, '知识不存在') + } + + const knowledge = await Knowledge.findById(req.params.id).lean() + if (!knowledge) { + return ApiResponse.notFound(res, '知识不存在') + } + if (!Array.isArray(knowledge.options) || knowledge.options.length < 2) { + return ApiResponse.error(res, '该知识暂无检测题', 400) + } + + const { optionIndex } = req.body + const idx = parseInt(optionIndex) + if (Number.isNaN(idx) || idx < 0 || idx >= knowledge.options.length) { + return ApiResponse.error(res, '无效的选项', 400) + } + + const correctIndex = knowledge.options.findIndex(o => o.isCorrect) + const correct = idx === correctIndex + + let lit = false + let totalLit = 0 + let streak = 0 + + if (correct && req.user) { + const openid = req.openid + await KnowledgeLit.findOneAndUpdate( + { openid, knowledgeId: knowledge._id }, + { openid, knowledgeId: knowledge._id, dim: knowledge.dim, quizPassed: true, litAt: new Date() }, + { upsert: true } + ) + lit = true + const lits = await KnowledgeLit.find({ openid }).lean() + totalLit = lits.length + streak = calcStreak(lits.map(l => l.litAt)) + } + + return ApiResponse.success(res, { + correct, + correctIndex, + answer: knowledge.answer || '', + lit, + totalLit, + streak + }) + } catch (error) { + logger.error('检测题作答失败:', error) + return ApiResponse.serverError(res, '操作失败') + } +}) + +/** + * 获取某维度的问答列表 + * GET /api/knowledge/:dim/questions + */ +router.get('/:dim/questions', async (req, res) => { + try { + const { dim } = req.params + const { limit = 10 } = req.query + + const questions = await Knowledge.find({ + dim: parseInt(dim), + category: 'question', + status: 'approved' + }) + .select('question options') + .limit(parseInt(limit)) + .lean() + + return ApiResponse.success(res, questions) + } catch (error) { + logger.error('获取问答失败:', error) + return ApiResponse.serverError(res, '获取问答失败') + } +}) + +module.exports = router diff --git a/server/src/routes/seo.js b/server/src/routes/seo.js index 4839768..3a9ec9d 100644 --- a/server/src/routes/seo.js +++ b/server/src/routes/seo.js @@ -1,13 +1,14 @@ -import express from 'express' -import fs from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const express = require('express') const router = express.Router() -const SITE = process.env.SITE_URL || '' -const origin = (req) => (SITE ? SITE.replace(/\/$/, '') : `${req.protocol}://${req.get('host')}`) +// 反代场景下优先取 x-forwarded 头,保证 sitemap/robots 里的站点地址正确 +function origin(req) { + const site = process.env.SITE_URL || '' + if (site) return site.replace(/\/$/, '') + const proto = (req.headers['x-forwarded-proto'] || req.protocol || 'http').split(',')[0].trim() + const host = req.headers['x-forwarded-host'] || req.headers.host || 'localhost' + return proto + '://' + host +} const DESC = '宇之然AI维度:从五个维度(AI起源、发展、当前格局、学习工具、趋势跟踪)系统理解人工智能的知识探索社区。每日更新AI行业趋势与热点,支持问答、收藏与学习进度跟踪。' @@ -99,4 +100,4 @@ router.get('/about', (req, res) => { `) }) -export default router +module.exports = router diff --git a/server/src/scripts/generate-quiz.js b/server/src/scripts/generate-quiz.js new file mode 100644 index 0000000..fac6400 --- /dev/null +++ b/server/src/scripts/generate-quiz.js @@ -0,0 +1,153 @@ +/** + * AI 批量生成知识检测题 —— 为没有 options 的知识生成 1 道四选一检测题 + * 执行: node src/scripts/generate-quiz.js [--dry] [--limit 10] + * + * 生成后字段: question / options[{text,isCorrect}] / answer(解析) + * 幂等:已有 options 的知识自动跳过;单个失败不影响后续 + */ +const mongoose = require('mongoose') +const path = require('path') +require('dotenv').config({ path: path.join(__dirname, '..', '..', '.env') }) + +const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/wdkj' + +const args = process.argv.slice(2) +const DRY = args.includes('--dry') +const LIMIT_ARG = args.find(a => a.startsWith('--limit')) +const LIMIT = LIMIT_ARG ? parseInt(LIMIT_ARG.split('=')[1] || args[args.indexOf(LIMIT_ARG) + 1]) : 0 + +async function getModels() { + const { AIModel } = require('../models') + const active = await AIModel.getActiveModels() + if (active && active.length) return active + return [{ + name: 'env-model', + apiUrl: process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions', + apiKey: process.env.OPENAI_API_KEY, + modelId: process.env.OPENAI_MODEL || 'gpt-3.5-turbo' + }] +} + +function extractJson(text) { + if (!text) return null + let t = text.trim() + const fence = t.match(/```(?:json)?\s*([\s\S]*?)```/) + if (fence) t = fence[1].trim() + const start = t.indexOf('{') + const end = t.lastIndexOf('}') + if (start === -1 || end === -1) return null + try { + return JSON.parse(t.slice(start, end + 1)) + } catch { + return null + } +} + +function validate(q) { + if (!q || typeof q.question !== 'string' || !q.question.trim()) return null + if (!Array.isArray(q.options) || q.options.length < 3 || q.options.length > 5) return null + const normalized = q.options.map(o => ({ text: String(o.text || '').trim(), isCorrect: !!o.isCorrect })) + if (normalized.some(o => !o.text)) return null + const correctCount = normalized.filter(o => o.isCorrect).length + if (correctCount !== 1) return null + return { + question: q.question.trim(), + options: normalized, + answer: String(q.answer || '').trim() + } +} + +async function askAI(models, prompt) { + for (const model of models) { + try { + const headers = { 'Content-Type': 'application/json' } + if (model.apiKey && model.apiKey !== 'sk-dummy' && model.apiKey !== '***') { + headers['Authorization'] = `Bearer ${model.apiKey}` + } + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), 60000) + let res + try { + res = await fetch(model.apiUrl, { + method: 'POST', + headers, + body: JSON.stringify({ + model: model.modelId, + messages: [ + { role: 'system', content: '你是知识测验出题官,只输出 JSON,不要输出任何其他文字。' }, + { role: 'user', content: prompt } + ], + temperature: 0.4, + max_tokens: 600 + }), + signal: controller.signal + }) + } finally { + clearTimeout(timer) + } + if (!res.ok) continue + const data = await res.json() + const content = data.choices?.[0]?.message?.content || '' + return content + } catch (e) { + console.warn(` 模型 ${model.name} 失败: ${e.message}`) + } + } + return null +} + +async function main() { + await mongoose.connect(MONGODB_URI) + console.log('✅ 已连接 MongoDB') + + const { Knowledge } = require('../models') + const models = await getModels() + console.log(`🤖 出题模型: ${models.map(m => m.name).join(', ')}`) + + const query = { + status: 'approved', + $or: [{ options: { $exists: false } }, { options: { $size: 0 } }, { question: { $exists: false } }, { question: null }] + } + let items = await Knowledge.find(query).sort({ dim: 1, sortOrder: 1 }).lean() + if (LIMIT > 0) items = items.slice(0, LIMIT) + console.log(`📚 待出题知识: ${items.length} 条${DRY ? '(dry 模式,不写库)' : ''}\n`) + + let ok = 0 + let fail = 0 + for (const item of items) { + const prompt = `基于以下 AI 知识内容,出一道四选一的中文检测题(考察对核心概念的理解,干扰项要有迷惑性但不歧义)。 + +标题:${item.title} +内容:${item.content.slice(0, 800)} + +严格按此 JSON 格式输出: +{"question":"题干","options":[{"text":"选项A","isCorrect":true},{"text":"选项B","isCorrect":false},{"text":"选项C","isCorrect":false},{"text":"选项D","isCorrect":false}],"answer":"一句话解析为什么正确"}` + + console.log(`→ [dim${item.dim}] ${item.title}`) + const raw = await askAI(models, prompt) + const quiz = validate(extractJson(raw)) + + if (!quiz) { + console.log(' ✗ 生成或校验失败,跳过') + fail++ + } else { + console.log(` ✓ ${quiz.question} (${quiz.options.length} 选项)`) + if (!DRY) { + await Knowledge.updateOne( + { _id: item._id }, + { $set: { question: quiz.question, options: quiz.options, answer: quiz.answer } } + ) + } + ok++ + } + await new Promise(r => setTimeout(r, 400)) + } + + console.log(`\n完成:成功 ${ok},失败 ${fail},共 ${items.length}`) + await mongoose.disconnect() +} + +main().catch(err => { + console.error('执行失败:', err) + process.exit(1) +})