sg-paisou 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.idea/GitCommitMessageStorage.xml +8 -0
  2. package/.idea/modules.xml +8 -0
  3. package/.idea/sg-paisou-web.iml +12 -0
  4. package/.idea/vcs.xml +6 -0
  5. package/.vscode/settings.json +2 -0
  6. package/README.md +2 -0
  7. package/auto-imports.d.ts +10 -0
  8. package/components.d.ts +18 -0
  9. package/index.html +21 -0
  10. package/package.json +39 -0
  11. package/public/mathjax/all-packages.js +34 -0
  12. package/public/mathjax/talEditorConfig.js +5 -0
  13. package/public/mathjax/tex-svg.js +3 -0
  14. package/src/App.vue +38 -0
  15. package/src/assets/images/Camera-icon.png +0 -0
  16. package/src/assets/images/anger.png +0 -0
  17. package/src/assets/images/answer-icon.png +0 -0
  18. package/src/assets/images/back.png +0 -0
  19. package/src/assets/images/bg-img.png +0 -0
  20. package/src/assets/images/collect.png +0 -0
  21. package/src/assets/images/collected.png +0 -0
  22. package/src/assets/images/empty-bookmark.png +0 -0
  23. package/src/assets/images/empty-history.png +0 -0
  24. package/src/assets/images/feedback-thinkie-img.png +0 -0
  25. package/src/assets/images/history.png +0 -0
  26. package/src/assets/images/image 5.png +0 -0
  27. package/src/assets/images/insolubility.png +0 -0
  28. package/src/assets/images/key-points.png +0 -0
  29. package/src/assets/images/photograph-icon.png +0 -0
  30. package/src/assets/images/praise-icon.png +0 -0
  31. package/src/assets/images/praiseing-icon.png +0 -0
  32. package/src/assets/images/question.png +0 -0
  33. package/src/assets/images/smiling.png +0 -0
  34. package/src/assets/images/solution-icon.png +0 -0
  35. package/src/assets/images/star-icon.png +0 -0
  36. package/src/assets/images/trample-icon.png +0 -0
  37. package/src/assets/images/trampleing-icon.png +0 -0
  38. package/src/assets/images/volume-icon.png +0 -0
  39. package/src/assets/images/volumeing-icon.png +0 -0
  40. package/src/components/AIChatSDK/AIChatComponent.vue +963 -0
  41. package/src/components/AIChatSDK/component/Dialogs.vue +340 -0
  42. package/src/components/AIChatSDK/component/SpecialQuestions.vue +208 -0
  43. package/src/components/AIChatSDK/index.ts +146 -0
  44. package/src/components/AIChatSDK/style.scss +432 -0
  45. package/src/components/AIChatSDK/types.ts +275 -0
  46. package/src/components/AIChatSDK/utils/imageUtils.ts +61 -0
  47. package/src/components/AIChatSDK/utils/latex.ts +34 -0
  48. package/src/components/AIChatSDK/utils/mergeConfig.ts +125 -0
  49. package/src/components/ImagePreview.vue +62 -0
  50. package/src/components/PageHeader/index.vue +121 -0
  51. package/src/config.ts +11 -0
  52. package/src/env.d.ts +11 -0
  53. package/src/main.ts +12 -0
  54. package/src/router.ts +20 -0
  55. package/src/style.css +33 -0
  56. package/src/type.ts +106 -0
  57. package/src/utils/TTS_README.md +232 -0
  58. package/src/utils/bridge.ts +42 -0
  59. package/src/utils/index.ts +8 -0
  60. package/src/utils/listenOsEvent.ts +3 -0
  61. package/src/utils/messageToast.ts +43 -0
  62. package/src/utils/render.ts +81 -0
  63. package/src/utils/request.ts +87 -0
  64. package/src/utils/tts.ts +319 -0
  65. package/src/utils/typewriter.ts +61 -0
  66. package/src/utils/useSSE.ts +113 -0
  67. package/src/views/History/index.vue +419 -0
  68. package/src/views/QuestionChatPage/index.vue +480 -0
  69. package/src/vite-env.d.ts +1 -0
  70. package/tsconfig.app.json +24 -0
  71. package/tsconfig.json +7 -0
  72. package/tsconfig.node.json +22 -0
  73. package/vite.config.ts +41 -0
@@ -0,0 +1,419 @@
1
+ <template>
2
+ <div class="history-page">
3
+ <PageHeader title="History" :back-type="fromSource" />
4
+
5
+ <div class="tabs">
6
+ <div
7
+ v-for="tab in tabs"
8
+ :key="tab.value"
9
+ :class="['tab-item', { active: activeTab === tab.value }]"
10
+ @click="activeTab = tab.value"
11
+ >
12
+ {{ tab.label }}
13
+ </div>
14
+ </div>
15
+
16
+ <div class="content" ref="contentRef" @scroll="handleScroll">
17
+ <div class="content-list">
18
+ <!--首次加载状态-->
19
+ <div v-if="isInitialLoading" class="loading-indicator">
20
+ <div class="loading-spinner"></div>
21
+ <span>Loading...</span>
22
+ </div>
23
+
24
+ <!-- 数据列表 -->
25
+ <template v-else-if="currentGroups.length > 0">
26
+ <div v-for="(group, index) in currentGroups" :key="index" class="date-group">
27
+ <div class="date-title">{{ group.date }}</div>
28
+ <div class="items-grid">
29
+ <div
30
+ v-for="item in group.items"
31
+ :key="item.id"
32
+ class="history-item"
33
+ @click="handleItemClick(item)"
34
+ >
35
+ <div class="item-image-wrapper">
36
+ <img :src="item.imageUrl" :alt="item.title" class="item-image" />
37
+ <div v-if="item.isBookmarked" class="bookmark-badge">Bookmarked</div>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </div>
42
+
43
+ <!-- 加载更多提示 -->
44
+ <div v-if="loading" class="loading-indicator">
45
+ <div class="loading-spinner"></div>
46
+ <span>Loading...</span>
47
+ </div>
48
+
49
+ <!-- 没有更多数据 -->
50
+ <div v-if="!hasMore" class="no-more-data">
51
+ No more data
52
+ </div>
53
+ </template>
54
+
55
+ <!-- 空状态(非首次加载且无数据) -->
56
+ <div v-else class="empty-state">
57
+ <img :src="emptyConfig.image" :alt="emptyConfig.alt" />
58
+ <span>{{ emptyConfig.text }}</span>
59
+ <span v-if="emptyConfig.description" class="empty-state-description">
60
+ {{ emptyConfig.description }}
61
+ </span>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </template>
67
+
68
+ <script lang="ts" setup>
69
+ import { ref, computed, onMounted, watch } from 'vue'
70
+ import { useRoute, useRouter } from 'vue-router'
71
+ import PageHeader from '../../components/PageHeader/index.vue'
72
+ import type { IHistoryItem, IHistoryGroup, ISessionItem } from '../../type'
73
+ import { getSessions, getFavorites } from '../../utils/request'
74
+ import emptyHistoryImg from '@/assets/images/empty-history.png'
75
+ import emptyBookmarkImg from '@/assets/images/empty-bookmark.png'
76
+
77
+ const route = useRoute()
78
+ const router = useRouter()
79
+
80
+ // Tab 配置
81
+ const tabs = [
82
+ { label: 'All', value: 'all' as const },
83
+ { label: 'Bookmarks', value: 'bookmarks' as const }
84
+ ]
85
+
86
+ const activeTab = ref<'all' | 'bookmarks'>('all')
87
+ const fromSource = ref<'client' | 'page'>(route.query.from === 'page' ? 'page' : 'client')
88
+ const contentRef = ref<HTMLElement>()
89
+ const loading = ref(false)
90
+ const pageSize = 10
91
+
92
+ // 为每个 Tab 独立维护分页状态
93
+ const pagingState = ref({
94
+ all: { currentPage: 1, hasMore: true, total: 0 },
95
+ bookmarks: { currentPage: 1, hasMore: true, total: 0 }
96
+ })
97
+
98
+ // 为每个 Tab 独立维护初始加载状态
99
+ const initialLoadingState = ref({
100
+ all: true,
101
+ bookmarks: true
102
+ })
103
+
104
+ // 数据存储
105
+ const historyData = ref<IHistoryItem[]>([])
106
+ const bookmarksData = ref<IHistoryItem[]>([])
107
+
108
+ // 当前 Tab 的分页状态
109
+ const currentPaging = computed(() => pagingState.value[activeTab.value])
110
+ const hasMore = computed(() => currentPaging.value.hasMore)
111
+ const isInitialLoading = computed(() => initialLoadingState.value[activeTab.value])
112
+
113
+ // 公共数据分组函数
114
+ const groupByDate = (items: IHistoryItem[]): IHistoryGroup[] => {
115
+ const groups: Record<string, IHistoryItem[]> = {}
116
+ items.forEach(item => {
117
+ (groups[item.date] ||= []).push(item)
118
+ })
119
+ return Object.entries(groups).map(([date, items]) => ({ date, items }))
120
+ }
121
+
122
+ // 当前 Tab 的分组数据
123
+ const currentGroups = computed(() =>
124
+ groupByDate(activeTab.value === 'all' ? historyData.value : bookmarksData.value)
125
+ )
126
+
127
+ // 空状态配置
128
+ const emptyConfig = computed(() =>
129
+ activeTab.value === 'all'
130
+ ? {
131
+ image: emptyHistoryImg,
132
+ alt: 'empty-history',
133
+ text: 'No search history.'
134
+ }
135
+ : {
136
+ image: emptyBookmarkImg,
137
+ alt: 'empty-bookmark',
138
+ text: 'No bookmarks yet',
139
+ description: 'Find a result and tap the bookmark icon to save it here'
140
+ }
141
+ )
142
+
143
+
144
+ // 格式化日期
145
+ const formatDate = (dateString: string): string => {
146
+ const date = new Date(dateString)
147
+ const today = new Date()
148
+ const dateStr = date.toDateString()
149
+
150
+ if (dateStr === today.toDateString()) return 'Today'
151
+
152
+ const yesterday = new Date(today)
153
+ yesterday.setDate(yesterday.getDate() - 1)
154
+ if (dateStr === yesterday.toDateString()) return 'Yesterday'
155
+
156
+ return `${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}/${date.getFullYear()}`
157
+ }
158
+
159
+ // 判断是否超过7天
160
+ const isOverSevenDays = (dateString: string): 0 | 1 => {
161
+ const diffDays = Math.floor((Date.now() - new Date(dateString).getTime()) / (86400000))
162
+ return diffDays > 7 ? 1 : 0
163
+ }
164
+
165
+ // 转换 API 数据
166
+ const convertSessionToHistoryItem = (session: ISessionItem): IHistoryItem => ({
167
+ id: session.id,
168
+ title: session.content || session.summary || session.orcText || 'Untitled',
169
+ imageUrl: session.imagePath || '',
170
+ isBookmarked: session.favorite === 1,
171
+ date: formatDate(session.createdAt),
172
+ isOverSevenDays: isOverSevenDays(session.createdAt),
173
+ questionData: session
174
+ })
175
+
176
+ // 点击历史记录项
177
+ const handleItemClick = (item: IHistoryItem) => {
178
+ try {
179
+ sessionStorage.setItem('selectedHistoryItem', JSON.stringify(item))
180
+ router.push({
181
+ path: '/',
182
+ query: { from: 'history', itemId: item.id, isOverSevenDays: String(item.isOverSevenDays) }
183
+ })
184
+ } catch (error) {
185
+ console.error('存储历史记录项数据失败:', error)
186
+ }
187
+ }
188
+
189
+ // 加载数据(统一处理历史记录和书签)
190
+ const loadData = async (type: 'all' | 'bookmarks', page: number = 1) => {
191
+ loading.value = true
192
+
193
+ try {
194
+ const apiCall = type === 'all' ? getSessions : getFavorites
195
+ const response = await apiCall(
196
+ { page, pageSize },
197
+ )
198
+
199
+ if (response?.sessions) {
200
+ const newItems = response.sessions.map(convertSessionToHistoryItem)
201
+ const targetData = type === 'all' ? historyData : bookmarksData
202
+
203
+ targetData.value = page === 1 ? newItems : [...targetData.value, ...newItems]
204
+
205
+ // 更新对应 Tab 的分页状态
206
+ pagingState.value[type].total = response.total
207
+ pagingState.value[type].currentPage = page
208
+ pagingState.value[type].hasMore = targetData.value.length < response.total
209
+
210
+ // 首次加载完成,关闭初始加载状态
211
+ if (page === 1) {
212
+ initialLoadingState.value[type] = false
213
+ }
214
+
215
+ console.log(`加载${type === 'all' ? '历史记录' : '书签'}第 ${page} 页,当前共 ${targetData.value.length}/${response.total} 条`)
216
+ } else {
217
+ // 如果没有数据,也关闭初始加载状态
218
+ if (page === 1) {
219
+ initialLoadingState.value[type] = false
220
+ }
221
+ }
222
+ } catch (error) {
223
+ console.error(`加载${type === 'all' ? '历史记录' : '书签'}失败:`, error)
224
+ pagingState.value[type].hasMore = false
225
+ // 加载失败时也关闭初始加载状态,以便显示空状态
226
+ if (page === 1) {
227
+ initialLoadingState.value[type] = false
228
+ }
229
+ } finally {
230
+ loading.value = false
231
+ }
232
+ }
233
+
234
+ // 加载更多数据
235
+ const loadMore = async () => {
236
+ if (loading.value || !currentPaging.value.hasMore) return
237
+
238
+ const nextPage = currentPaging.value.currentPage + 1
239
+ await loadData(activeTab.value, nextPage)
240
+ }
241
+
242
+ // 处理滚动事件
243
+ const handleScroll = (event: Event) => {
244
+ const target = event.target as HTMLElement
245
+ if (!target) return
246
+
247
+ const { scrollTop, scrollHeight, clientHeight } = target
248
+ if (scrollHeight - scrollTop - clientHeight < 100) {
249
+ loadMore()
250
+ }
251
+ }
252
+
253
+ // 监听 Tab 切换
254
+ watch(activeTab, async (newTab) => {
255
+ // 重置滚动位置
256
+ contentRef.value && (contentRef.value.scrollTop = 0)
257
+
258
+ // 如果该 Tab 还没有数据,则加载第一页
259
+ const targetData = newTab === 'all' ? historyData : bookmarksData
260
+ if (targetData.value.length === 0) {
261
+ // 如果还没有数据,确保初始加载状态为 true
262
+ initialLoadingState.value[newTab] = true
263
+ await loadData(newTab, 1)
264
+ } else {
265
+ // 如果已有数据,确保初始加载状态为 false
266
+ initialLoadingState.value[newTab] = false
267
+ }
268
+ })
269
+
270
+ onMounted(() => {
271
+ loadData('all', 1)
272
+ })
273
+
274
+ </script>
275
+
276
+ <style scoped lang="scss">
277
+ .history-page {
278
+ width: 100%;
279
+ height: 100vh;
280
+ display: flex;
281
+ flex-direction: column;
282
+ background: linear-gradient(rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.7));
283
+
284
+ .tabs {
285
+ display: flex;
286
+ gap: 12px;
287
+ padding: 0 48px 12px;
288
+
289
+ .tab-item {
290
+ padding: 8px 16px;
291
+ font-size: 16px;
292
+ font-weight: 500;
293
+ color: #666;
294
+ background: #FFF;
295
+ border-radius: 70px;
296
+ line-height: 24px;
297
+
298
+ &.active {
299
+ color: #FF9D00;
300
+ background: rgba(255, 157, 0, 0.1);
301
+ }
302
+ }
303
+ }
304
+
305
+ .content {
306
+ flex: 1;
307
+ padding: 12px 48px 24px;
308
+ overflow-y: auto;
309
+
310
+ .content-list {
311
+ display: flex;
312
+ flex-direction: column;
313
+ gap: 24px;
314
+ min-height: 100%;
315
+
316
+ .date-group {
317
+ .date-title {
318
+ font-size: 20px;
319
+ font-weight: 600;
320
+ color: #222;
321
+ line-height: 32px;
322
+ margin-bottom: 12px;
323
+ }
324
+
325
+ .items-grid {
326
+ display: grid;
327
+ grid-template-columns: repeat(2, 1fr);
328
+ gap: 24px;
329
+
330
+ .history-item {
331
+
332
+ .item-image-wrapper {
333
+ position: relative;
334
+ height: 180px;
335
+ border-radius: 12px;
336
+ overflow: hidden;
337
+
338
+ .item-image {
339
+ width: 100%;
340
+ height: 100%;
341
+ object-fit: cover;
342
+ }
343
+
344
+ .bookmark-badge {
345
+ position: absolute;
346
+ top: 16px;
347
+ left: 16px;
348
+ font-size: 18px;
349
+ line-height: 18px;
350
+ color: #FFF;
351
+ }
352
+ }
353
+ }
354
+ }
355
+ }
356
+
357
+ .empty-state {
358
+ display: flex;
359
+ flex-direction: column;
360
+ align-items: center;
361
+ justify-content: center;
362
+ gap: 20px;
363
+ flex: 1;
364
+
365
+ img {
366
+ width: 300px;
367
+ height: 200px;
368
+ }
369
+
370
+ span {
371
+ font-size: 20px;
372
+ font-weight: 500;
373
+ line-height: 24px;
374
+ color: #222;
375
+
376
+ &.empty-state-description {
377
+ font-size: 16px;
378
+ color: #999;
379
+ margin-top: -8px;
380
+ }
381
+ }
382
+ }
383
+
384
+ .loading-indicator {
385
+ display: flex;
386
+ flex-direction: column;
387
+ align-items: center;
388
+ justify-content: center;
389
+ gap: 12px;
390
+ text-align: center;
391
+ font-size: 16px;
392
+ color: #666;
393
+ min-height: 200px;
394
+
395
+ .loading-spinner {
396
+ width: 20px;
397
+ height: 20px;
398
+ border: 4px solid rgba(255, 157, 0, 0.2);
399
+ border-top-color: #FF9D00;
400
+ border-radius: 50%;
401
+ animation: spin 1s linear infinite;
402
+ }
403
+ }
404
+
405
+ .no-more-data {
406
+ text-align: center;
407
+ padding-bottom: 24px;
408
+ font-size: 16px;
409
+ color: #999;
410
+ }
411
+ }
412
+ }
413
+ }
414
+
415
+ @keyframes spin {
416
+ to { transform: rotate(360deg); }
417
+ }
418
+ </style>
419
+