@allbluecn/web-app 0.5.0 → 0.5.1

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 (69) hide show
  1. package/package.json +11 -7
  2. package/src/components/knowledge/knowledge-toolbar.tsx +124 -1
  3. package/src/components/story/constants.ts +11 -1
  4. package/src/components/story/create-story-dialog.tsx +12 -6
  5. package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +114 -0
  6. package/src/components/story/detail/activity-timeline.tsx +15 -0
  7. package/src/components/story/detail/story-detail-body.tsx +1 -0
  8. package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +748 -0
  9. package/src/components/story/detail/story-detail-toolbar/categories.tsx +23 -18
  10. package/src/components/story/detail/story-detail-toolbar/children.tsx +2 -0
  11. package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +29 -21
  12. package/src/components/story/detail/story-detail-toolbar.tsx +154 -8
  13. package/src/components/story/inline-editors/story-select-dialog.tsx +19 -4
  14. package/src/components/story/relations/__tests__/attachment-manage-dialog.test.tsx +171 -0
  15. package/src/components/story/relations/__tests__/knowledge-select-dialog.test.tsx +174 -0
  16. package/src/components/story/relations/__tests__/link-manage-dialog.test.tsx +93 -0
  17. package/src/components/story/relations/attachment-manage-dialog.tsx +490 -0
  18. package/src/components/story/relations/file-preview.tsx +173 -0
  19. package/src/components/story/relations/format.ts +120 -0
  20. package/src/components/story/relations/general-groups.ts +32 -0
  21. package/src/components/story/relations/knowledge-select-dialog.tsx +403 -0
  22. package/src/components/story/relations/link-manage-dialog.tsx +191 -0
  23. package/src/components/story/types.ts +13 -0
  24. package/src/lib/changelog/sdk-versions.ts +19 -19
  25. package/src/lib/story/__tests__/video-embed.test.ts +77 -0
  26. package/src/lib/story/video-embed.ts +59 -0
  27. package/src/plugins/.gen-manifest.json +13 -0
  28. package/src/plugins/modules.gen.ts +20 -0
  29. package/src/plugins/plugins.gen.ts +20 -0
  30. package/src/plugins/ui.gen.ts +8 -0
  31. package/src/routeTree.gen.ts +42 -0
  32. package/src/routes/_nav/inspiration/route.tsx +8 -0
  33. package/src/routes/_nav/prd/$id/route.tsx +9 -0
  34. package/src/routes/_nav/prd/index.tsx +9 -0
  35. package/src/routes/_nav/prd/route.tsx +8 -0
  36. package/src/routes/_nav/projects/$projectId/settings/route.lazy.tsx +74 -3
  37. package/src/routes/_nav/projects/$projectId/settings/route.tsx +6 -2
  38. package/src/routes/_nav/tags/$id/route.tsx +8 -0
  39. package/src/routes/_nav/tags/index.tsx +8 -0
  40. package/src/routes/_nav/tags/route.tsx +8 -0
  41. package/src/routes/_nav/tags/settings/route.tsx +9 -0
  42. package/src/routes/api/attachments/$id/content.ts +60 -0
  43. package/src/routes/api/attachments/upload.ts +148 -0
  44. package/src/routes/share/prd/$prdId/route.tsx +10 -0
  45. package/src/server/serverFns/__tests__/project-kb.test.ts +147 -0
  46. package/src/server/serverFns/knowledge.ts +50 -4
  47. package/src/server/serverFns/project.ts +43 -0
  48. package/src/server/serverFns/story/__tests__/kb-scope.test.ts +56 -0
  49. package/src/server/serverFns/story/__tests__/story-assoc.test.ts +222 -0
  50. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +26 -0
  51. package/src/server/serverFns/story/index.ts +6 -4
  52. package/src/server/serverFns/story/kb-general.ts +39 -0
  53. package/src/server/serverFns/story/kb-scope.ts +52 -0
  54. package/src/server/serverFns/story/link-preview.ts +115 -0
  55. package/src/server/serverFns/story/story-assoc.ts +339 -50
  56. package/src/server/serverFns/story/story-attachments.ts +12 -43
  57. package/src/server/serverFns/story/story-crud.ts +21 -4
  58. package/src/server/serverFns/story/story-dependencies.ts +35 -3
  59. package/src/server/serverFns/story/story-links.ts +13 -1
  60. package/src/server/serverFns/story/story-tree.ts +28 -1
  61. package/src/server/serverFns/story/types.ts +37 -0
  62. package/src/styles/globals.css +25 -0
  63. package/vite.shared.ts +2 -0
  64. package/src/components/story/detail/story-detail-toolbar/attachments.tsx +0 -73
  65. package/src/components/story/detail/story-detail-toolbar/knowledge.tsx +0 -73
  66. package/src/components/story/detail/story-detail-toolbar/links.tsx +0 -73
  67. package/src/components/story/relations/attachments-panel.tsx +0 -179
  68. package/src/components/story/relations/knowledge-panel.tsx +0 -284
  69. package/src/components/story/relations/links-panel.tsx +0 -240
@@ -82,6 +82,7 @@ import { Route as NavTagsIdRouteRouteImport } from './routes/_nav/tags/$id/route
82
82
  import { Route as NavTagsSettingsRouteRouteImport } from './routes/_nav/tags/settings/route'
83
83
  import { Route as NavWorkspacesIndexRouteImport } from './routes/_nav/workspaces/index'
84
84
  import { Route as NavWorkspacesWorkspaceIdRouteRouteImport } from './routes/_nav/workspaces/$workspaceId/route'
85
+ import { Route as ApiAttachmentsUploadRouteImport } from './routes/api/attachments/upload'
85
86
  import { Route as ApiAuthSplatRouteImport } from './routes/api.auth.$'
86
87
  import { Route as ApiAuthDesktopCallbackRouteImport } from './routes/api/auth/desktop-callback'
87
88
  import { Route as ApiDesignUploadRouteImport } from './routes/api/design/upload'
@@ -100,6 +101,7 @@ import { Route as NavProjectsProjectIdStoryIdRouteRouteImport } from './routes/_
100
101
  import { Route as NavProjectsProjectIdSettingsRouteRouteImport } from './routes/_nav/projects/$projectId/settings/route'
101
102
  import { Route as NavSettingsAiProviderIdRouteRouteImport } from './routes/_nav/settings/ai/$providerId/route'
102
103
  import { Route as NavWorkspacesWorkspaceIdSettingsRouteRouteImport } from './routes/_nav/workspaces/$workspaceId/settings/route'
104
+ import { Route as ApiAttachmentsIdContentRouteImport } from './routes/api/attachments/$id/content'
103
105
  import { Route as DesignFilesProjectIdArtboardIdFilenameRouteImport } from './routes/design-files/$projectId/$artboardId/$filename'
104
106
  import { Route as NavKnowledgeKbIdDocDocIdRouteRouteImport } from './routes/_nav/knowledge/$kbId/doc/$docId/route'
105
107
  import { Route as ApiSharePrdPrdIdEventsRouteImport } from './routes/api/share/prd/$prdId/events'
@@ -575,6 +577,11 @@ const NavWorkspacesWorkspaceIdRouteRoute =
575
577
  (d) => d.Route,
576
578
  ),
577
579
  )
580
+ const ApiAttachmentsUploadRoute = ApiAttachmentsUploadRouteImport.update({
581
+ id: '/api/attachments/upload',
582
+ path: '/api/attachments/upload',
583
+ getParentRoute: () => rootRouteImport,
584
+ } as any)
578
585
  const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
579
586
  id: '/api/auth/$',
580
587
  path: '/api/auth/$',
@@ -790,6 +797,11 @@ const NavWorkspacesWorkspaceIdSettingsRouteRoute =
790
797
  (d) => d.Route,
791
798
  ),
792
799
  )
800
+ const ApiAttachmentsIdContentRoute = ApiAttachmentsIdContentRouteImport.update({
801
+ id: '/api/attachments/$id/content',
802
+ path: '/api/attachments/$id/content',
803
+ getParentRoute: () => rootRouteImport,
804
+ } as any)
793
805
  const DesignFilesProjectIdArtboardIdFilenameRoute =
794
806
  DesignFilesProjectIdArtboardIdFilenameRouteImport.update({
795
807
  id: '/design-files/$projectId/$artboardId/$filename',
@@ -898,6 +910,7 @@ export interface FileRoutesByFullPath {
898
910
  '/share/law/$id': typeof ShareLawIdRouteRoute
899
911
  '/share/prd/$prdId': typeof SharePrdPrdIdRouteRoute
900
912
  '/chat/$threadId': typeof NavChatThreadIdRoute
913
+ '/api/attachments/upload': typeof ApiAttachmentsUploadRoute
901
914
  '/api/auth/$': typeof ApiAuthSplatRoute
902
915
  '/api/auth/desktop-callback': typeof ApiAuthDesktopCallbackRoute
903
916
  '/api/design/upload': typeof ApiDesignUploadRoute
@@ -916,6 +929,7 @@ export interface FileRoutesByFullPath {
916
929
  '/projects/$projectId/settings': typeof NavProjectsProjectIdSettingsRouteRoute
917
930
  '/settings/ai/$providerId': typeof NavSettingsAiProviderIdRouteRoute
918
931
  '/workspaces/$workspaceId/settings': typeof NavWorkspacesWorkspaceIdSettingsRouteRoute
932
+ '/api/attachments/$id/content': typeof ApiAttachmentsIdContentRoute
919
933
  '/design-files/$projectId/$artboardId/$filename': typeof DesignFilesProjectIdArtboardIdFilenameRoute
920
934
  '/settings/ai/': typeof NavSettingsAiIndexRouteLazyRoute
921
935
  '/knowledge/bookmarks/$id': typeof NavKnowledgeBookmarksIdLazyRoute
@@ -988,6 +1002,7 @@ export interface FileRoutesByTo {
988
1002
  '/share/law/$id': typeof ShareLawIdRouteRoute
989
1003
  '/share/prd/$prdId': typeof SharePrdPrdIdRouteRoute
990
1004
  '/chat/$threadId': typeof NavChatThreadIdRoute
1005
+ '/api/attachments/upload': typeof ApiAttachmentsUploadRoute
991
1006
  '/api/auth/$': typeof ApiAuthSplatRoute
992
1007
  '/api/auth/desktop-callback': typeof ApiAuthDesktopCallbackRoute
993
1008
  '/api/design/upload': typeof ApiDesignUploadRoute
@@ -1006,6 +1021,7 @@ export interface FileRoutesByTo {
1006
1021
  '/projects/$projectId/settings': typeof NavProjectsProjectIdSettingsRouteRoute
1007
1022
  '/settings/ai/$providerId': typeof NavSettingsAiProviderIdRouteRoute
1008
1023
  '/workspaces/$workspaceId/settings': typeof NavWorkspacesWorkspaceIdSettingsRouteRoute
1024
+ '/api/attachments/$id/content': typeof ApiAttachmentsIdContentRoute
1009
1025
  '/design-files/$projectId/$artboardId/$filename': typeof DesignFilesProjectIdArtboardIdFilenameRoute
1010
1026
  '/settings/ai': typeof NavSettingsAiIndexRouteLazyRoute
1011
1027
  '/knowledge/bookmarks/$id': typeof NavKnowledgeBookmarksIdLazyRoute
@@ -1096,6 +1112,7 @@ export interface FileRoutesById {
1096
1112
  '/share/law/$id': typeof ShareLawIdRouteRoute
1097
1113
  '/share/prd/$prdId': typeof SharePrdPrdIdRouteRoute
1098
1114
  '/_nav/chat/$threadId': typeof NavChatThreadIdRoute
1115
+ '/api/attachments/upload': typeof ApiAttachmentsUploadRoute
1099
1116
  '/api/auth/$': typeof ApiAuthSplatRoute
1100
1117
  '/api/auth/desktop-callback': typeof ApiAuthDesktopCallbackRoute
1101
1118
  '/api/design/upload': typeof ApiDesignUploadRoute
@@ -1114,6 +1131,7 @@ export interface FileRoutesById {
1114
1131
  '/_nav/projects/$projectId/settings': typeof NavProjectsProjectIdSettingsRouteRoute
1115
1132
  '/_nav/settings/ai/$providerId': typeof NavSettingsAiProviderIdRouteRoute
1116
1133
  '/_nav/workspaces/$workspaceId/settings': typeof NavWorkspacesWorkspaceIdSettingsRouteRoute
1134
+ '/api/attachments/$id/content': typeof ApiAttachmentsIdContentRoute
1117
1135
  '/design-files/$projectId/$artboardId/$filename': typeof DesignFilesProjectIdArtboardIdFilenameRoute
1118
1136
  '/_nav/settings/ai/': typeof NavSettingsAiIndexRouteLazyRoute
1119
1137
  '/_nav/knowledge/bookmarks/$id': typeof NavKnowledgeBookmarksIdLazyRoute
@@ -1202,6 +1220,7 @@ export interface FileRouteTypes {
1202
1220
  | '/share/law/$id'
1203
1221
  | '/share/prd/$prdId'
1204
1222
  | '/chat/$threadId'
1223
+ | '/api/attachments/upload'
1205
1224
  | '/api/auth/$'
1206
1225
  | '/api/auth/desktop-callback'
1207
1226
  | '/api/design/upload'
@@ -1220,6 +1239,7 @@ export interface FileRouteTypes {
1220
1239
  | '/projects/$projectId/settings'
1221
1240
  | '/settings/ai/$providerId'
1222
1241
  | '/workspaces/$workspaceId/settings'
1242
+ | '/api/attachments/$id/content'
1223
1243
  | '/design-files/$projectId/$artboardId/$filename'
1224
1244
  | '/settings/ai/'
1225
1245
  | '/knowledge/bookmarks/$id'
@@ -1292,6 +1312,7 @@ export interface FileRouteTypes {
1292
1312
  | '/share/law/$id'
1293
1313
  | '/share/prd/$prdId'
1294
1314
  | '/chat/$threadId'
1315
+ | '/api/attachments/upload'
1295
1316
  | '/api/auth/$'
1296
1317
  | '/api/auth/desktop-callback'
1297
1318
  | '/api/design/upload'
@@ -1310,6 +1331,7 @@ export interface FileRouteTypes {
1310
1331
  | '/projects/$projectId/settings'
1311
1332
  | '/settings/ai/$providerId'
1312
1333
  | '/workspaces/$workspaceId/settings'
1334
+ | '/api/attachments/$id/content'
1313
1335
  | '/design-files/$projectId/$artboardId/$filename'
1314
1336
  | '/settings/ai'
1315
1337
  | '/knowledge/bookmarks/$id'
@@ -1399,6 +1421,7 @@ export interface FileRouteTypes {
1399
1421
  | '/share/law/$id'
1400
1422
  | '/share/prd/$prdId'
1401
1423
  | '/_nav/chat/$threadId'
1424
+ | '/api/attachments/upload'
1402
1425
  | '/api/auth/$'
1403
1426
  | '/api/auth/desktop-callback'
1404
1427
  | '/api/design/upload'
@@ -1417,6 +1440,7 @@ export interface FileRouteTypes {
1417
1440
  | '/_nav/projects/$projectId/settings'
1418
1441
  | '/_nav/settings/ai/$providerId'
1419
1442
  | '/_nav/workspaces/$workspaceId/settings'
1443
+ | '/api/attachments/$id/content'
1420
1444
  | '/design-files/$projectId/$artboardId/$filename'
1421
1445
  | '/_nav/settings/ai/'
1422
1446
  | '/_nav/knowledge/bookmarks/$id'
@@ -1452,12 +1476,14 @@ export interface RootRouteChildren {
1452
1476
  ServerWsRoute: typeof ServerWsRoute
1453
1477
  ApiLicenseActivateRouteRoute: typeof ApiLicenseActivateRouteRoute
1454
1478
  ApiLicenseHeartbeatRouteRoute: typeof ApiLicenseHeartbeatRouteRoute
1479
+ ApiAttachmentsUploadRoute: typeof ApiAttachmentsUploadRoute
1455
1480
  ApiAuthSplatRoute: typeof ApiAuthSplatRoute
1456
1481
  ApiAuthDesktopCallbackRoute: typeof ApiAuthDesktopCallbackRoute
1457
1482
  ApiDesignUploadRoute: typeof ApiDesignUploadRoute
1458
1483
  ApiKnowledgeSyncGithubRoute: typeof ApiKnowledgeSyncGithubRoute
1459
1484
  ApiNotificationsStreamRoute: typeof ApiNotificationsStreamRoute
1460
1485
  ServerAnyhubProxySplatRoute: typeof ServerAnyhubProxySplatRoute
1486
+ ApiAttachmentsIdContentRoute: typeof ApiAttachmentsIdContentRoute
1461
1487
  DesignFilesProjectIdArtboardIdFilenameRoute: typeof DesignFilesProjectIdArtboardIdFilenameRoute
1462
1488
  ApiSharePrdPrdIdEventsRoute: typeof ApiSharePrdPrdIdEventsRoute
1463
1489
  }
@@ -1961,6 +1987,13 @@ declare module '@tanstack/react-router' {
1961
1987
  preLoaderRoute: typeof NavWorkspacesWorkspaceIdRouteRouteImport
1962
1988
  parentRoute: typeof NavWorkspacesRouteRoute
1963
1989
  }
1990
+ '/api/attachments/upload': {
1991
+ id: '/api/attachments/upload'
1992
+ path: '/api/attachments/upload'
1993
+ fullPath: '/api/attachments/upload'
1994
+ preLoaderRoute: typeof ApiAttachmentsUploadRouteImport
1995
+ parentRoute: typeof rootRouteImport
1996
+ }
1964
1997
  '/api/auth/$': {
1965
1998
  id: '/api/auth/$'
1966
1999
  path: '/api/auth/$'
@@ -2164,6 +2197,13 @@ declare module '@tanstack/react-router' {
2164
2197
  preLoaderRoute: typeof NavWorkspacesWorkspaceIdSettingsRouteRouteImport
2165
2198
  parentRoute: typeof NavWorkspacesWorkspaceIdRouteRoute
2166
2199
  }
2200
+ '/api/attachments/$id/content': {
2201
+ id: '/api/attachments/$id/content'
2202
+ path: '/api/attachments/$id/content'
2203
+ fullPath: '/api/attachments/$id/content'
2204
+ preLoaderRoute: typeof ApiAttachmentsIdContentRouteImport
2205
+ parentRoute: typeof rootRouteImport
2206
+ }
2167
2207
  '/design-files/$projectId/$artboardId/$filename': {
2168
2208
  id: '/design-files/$projectId/$artboardId/$filename'
2169
2209
  path: '/design-files/$projectId/$artboardId/$filename'
@@ -2651,12 +2691,14 @@ const rootRouteChildren: RootRouteChildren = {
2651
2691
  ServerWsRoute: ServerWsRoute,
2652
2692
  ApiLicenseActivateRouteRoute: ApiLicenseActivateRouteRoute,
2653
2693
  ApiLicenseHeartbeatRouteRoute: ApiLicenseHeartbeatRouteRoute,
2694
+ ApiAttachmentsUploadRoute: ApiAttachmentsUploadRoute,
2654
2695
  ApiAuthSplatRoute: ApiAuthSplatRoute,
2655
2696
  ApiAuthDesktopCallbackRoute: ApiAuthDesktopCallbackRoute,
2656
2697
  ApiDesignUploadRoute: ApiDesignUploadRoute,
2657
2698
  ApiKnowledgeSyncGithubRoute: ApiKnowledgeSyncGithubRoute,
2658
2699
  ApiNotificationsStreamRoute: ApiNotificationsStreamRoute,
2659
2700
  ServerAnyhubProxySplatRoute: ServerAnyhubProxySplatRoute,
2701
+ ApiAttachmentsIdContentRoute: ApiAttachmentsIdContentRoute,
2660
2702
  DesignFilesProjectIdArtboardIdFilenameRoute:
2661
2703
  DesignFilesProjectIdArtboardIdFilenameRoute,
2662
2704
  ApiSharePrdPrdIdEventsRoute: ApiSharePrdPrdIdEventsRoute,
@@ -0,0 +1,8 @@
1
+ // AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
2
+ import { createFileRoute } from '@tanstack/react-router'
3
+ import type { PluginRouteContext } from '@allbluecn/kernel'
4
+ import { inspiration_route_0 } from '@allbluecn/plugins-core/inspiration/routes'
5
+
6
+ export const Route = createFileRoute('/_nav/inspiration')({
7
+ component: inspiration_route_0.Component as any,
8
+ })
@@ -0,0 +1,9 @@
1
+ // AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
2
+ import { createFileRoute } from '@tanstack/react-router'
3
+ import type { PluginRouteContext } from '@allbluecn/kernel'
4
+ import { prd_route_2 } from '@allbluecn/plugins-core/prd/routes'
5
+
6
+ export const Route = createFileRoute('/_nav/prd/$id')({
7
+ beforeLoad: ((ctx: PluginRouteContext) => prd_route_2.beforeLoad(ctx)) as any,
8
+ component: prd_route_2.Component as any,
9
+ })
@@ -0,0 +1,9 @@
1
+ // AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
2
+ import { createFileRoute } from '@tanstack/react-router'
3
+ import type { PluginRouteContext } from '@allbluecn/kernel'
4
+ import { prd_route_1 } from '@allbluecn/plugins-core/prd/routes'
5
+
6
+ export const Route = createFileRoute('/_nav/prd/')({
7
+ loader: ((ctx: PluginRouteContext) => prd_route_1.loader(ctx)) as any,
8
+ component: prd_route_1.Component as any,
9
+ })
@@ -0,0 +1,8 @@
1
+ // AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
2
+ import { createFileRoute } from '@tanstack/react-router'
3
+ import type { PluginRouteContext } from '@allbluecn/kernel'
4
+ import { prd_route_0 } from '@allbluecn/plugins-core/prd/routes'
5
+
6
+ export const Route = createFileRoute('/_nav/prd')({
7
+ component: prd_route_0.Component as any,
8
+ })
@@ -19,19 +19,21 @@ import { createLazyFileRoute, Link } from "@tanstack/react-router";
19
19
  import { useRouter } from "@tanstack/react-router";
20
20
  import { useTranslation } from "react-i18next";
21
21
  import { useEffect, useState } from "react";
22
- import { ArrowLeft, Loader2 } from "lucide-react";
22
+ import { ArrowLeft, BookOpenText, Loader2 } from "lucide-react";
23
23
  import { toast } from '@/components/ui/sonner';
24
24
  import { Button } from "@/components/ui/button";
25
25
  import { Input } from "@/components/ui/input";
26
26
  import { Textarea } from "@/components/ui/textarea";
27
27
  import { Label } from "@/components/ui/label";
28
28
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
29
+ import { Checkbox } from "@/components/ui/checkbox";
30
+ import { Badge } from "@/components/ui/badge";
29
31
  import { IconPicker } from "@/components/shared/icon-picker";
30
32
  import { PROJECT_ICONS } from "@/components/shared/icon-packs/project";
31
33
  import { NameConfirmDialog } from "@/components/shared/name-confirm-dialog";
32
34
  import { ProjectMemberManager } from "@/components/project/project-member-manager";
33
35
  import { extractDisplayId, validateDisplayId } from "@/lib/project-display-id";
34
- import { checkDisplayIdAvailableFn, updateProjectFn, deleteProjectFn } from "@/server/serverFns/project";
36
+ import { checkDisplayIdAvailableFn, updateProjectFn, deleteProjectFn, setProjectKbsFn } from "@/server/serverFns/project";
35
37
  import { useTimezone } from "@/providers/timezone-provider";
36
38
  import { TimezoneSelect } from "@/components/shared/timezone-select";
37
39
  import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
@@ -39,7 +41,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
39
41
  function ProjectSettingsPage() {
40
42
  const { t } = useTranslation('projects');
41
43
  const router = useRouter();
42
- const { project, members } = Route.useLoaderData();
44
+ const { project, members, linkedKbIds, candidateKbs } = Route.useLoaderData();
43
45
  const session = Route.useRouteContext({ select: (c) => c.session });
44
46
  const currentUserId = session?.user?.id;
45
47
  const currentUserRole =
@@ -57,6 +59,8 @@ function ProjectSettingsPage() {
57
59
  const [projectTimezone, setProjectTimezone] = useState(project.timezone ?? "");
58
60
  const [saving, setSaving] = useState(false);
59
61
  const [deleteOpen, setDeleteOpen] = useState(false);
62
+ const [kbChecked, setKbChecked] = useState<Set<string>>(() => new Set(linkedKbIds));
63
+ const [savingKbs, setSavingKbs] = useState(false);
60
64
  const [deleteMode, setDeleteMode] = useState<"transfer" | "clear">("transfer");
61
65
  const userTimezone = useTimezone();
62
66
 
@@ -102,6 +106,18 @@ function ProjectSettingsPage() {
102
106
  }
103
107
  };
104
108
 
109
+ const saveKbs = async () => {
110
+ setSavingKbs(true);
111
+ try {
112
+ await setProjectKbsFn({ data: { projectId: project.id, kbIds: [...kbChecked] } });
113
+ toast.success(t('settings.kbSection.saved'));
114
+ } catch (err) {
115
+ toast.error((err as Error).message || t('settings.kbSection.saveFailed'));
116
+ } finally {
117
+ setSavingKbs(false);
118
+ }
119
+ };
120
+
105
121
  const handleDelete = async (mode: "transfer" | "clear") => {
106
122
  try {
107
123
  await deleteProjectFn({ data: { projectId: project.id, mode } })
@@ -225,6 +241,61 @@ function ProjectSettingsPage() {
225
241
  </Card>
226
242
  )}
227
243
 
244
+ <Card className="mb-6">
245
+ <CardHeader>
246
+ <CardTitle>{t('settings.kbSection.title')}</CardTitle>
247
+ </CardHeader>
248
+ <CardContent>
249
+ <p className="mb-3 text-sm text-muted-foreground">
250
+ {t('settings.kbSection.desc')}
251
+ </p>
252
+ {candidateKbs.filter((kb) => kb.visibility !== 'private').length === 0 && (
253
+ <p className="text-sm text-muted-foreground">{t('settings.kbSection.empty')}</p>
254
+ )}
255
+ <div className="flex flex-col gap-2">
256
+ {candidateKbs
257
+ .filter((kb) => kb.visibility !== 'private')
258
+ .map((kb) => {
259
+ const isGeneral = kb.visibility === 'general';
260
+ return (
261
+ <label
262
+ key={kb.id}
263
+ className={`flex items-center gap-2 rounded-md border px-3 py-2 text-sm ${
264
+ isGeneral ? 'opacity-60' : 'cursor-pointer'
265
+ }`}
266
+ >
267
+ <Checkbox
268
+ checked={isGeneral ? false : kbChecked.has(kb.id)}
269
+ disabled={isGeneral}
270
+ onCheckedChange={(v) =>
271
+ setKbChecked((prev) => {
272
+ const next = new Set(prev);
273
+ if (v) next.add(kb.id);
274
+ else next.delete(kb.id);
275
+ return next;
276
+ })
277
+ }
278
+ />
279
+ <BookOpenText className="size-4 text-muted-foreground" />
280
+ <span className="flex-1">{kb.name}</span>
281
+ {isGeneral ? (
282
+ <span className="text-xs text-muted-foreground">
283
+ {t('settings.kbSection.generalDisabled')}
284
+ </span>
285
+ ) : (
286
+ <Badge variant="outline" className="text-xs">{t('knowledge:toolbar.team')}</Badge>
287
+ )}
288
+ </label>
289
+ );
290
+ })}
291
+ </div>
292
+ <Button onClick={saveKbs} disabled={savingKbs} className="mt-3">
293
+ {savingKbs ? <Loader2 className="mr-2 size-4 animate-spin" /> : null}
294
+ {t('settings.kbSection.save')}
295
+ </Button>
296
+ </CardContent>
297
+ </Card>
298
+
228
299
  <Card className="mb-6">
229
300
  <CardHeader>
230
301
  <CardTitle>{t('settings.members')}</CardTitle>
@@ -19,14 +19,18 @@ import { createFileRoute } from "@tanstack/react-router";
19
19
  import {
20
20
  getProjectDetailFn,
21
21
  listProjectMembersFn,
22
+ listProjectKbsFn,
22
23
  } from "@/server/serverFns/project";
24
+ import { listKnowledgeFn } from "@/server/serverFns/knowledge";
23
25
 
24
26
  export const Route = createFileRoute("/_nav/projects/$projectId/settings")({
25
27
  loader: async ({ params }) => {
26
- const [project, members] = await Promise.all([
28
+ const [project, members, linkedKbs, myKbs] = await Promise.all([
27
29
  getProjectDetailFn({ data: { projectId: params.projectId } }),
28
30
  listProjectMembersFn({ data: { projectId: params.projectId } }),
31
+ listProjectKbsFn({ data: { projectId: params.projectId } }),
32
+ listKnowledgeFn({ data: {} }),
29
33
  ]);
30
- return { project, members };
34
+ return { project, members, linkedKbIds: linkedKbs.map((k) => k.id), candidateKbs: myKbs };
31
35
  },
32
36
  });
@@ -0,0 +1,8 @@
1
+ // AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
2
+ import { createFileRoute } from '@tanstack/react-router'
3
+ import type { PluginRouteContext } from '@allbluecn/kernel'
4
+ import { tags_route_2 } from '@allbluecn/plugins-core/tags/routes'
5
+
6
+ export const Route = createFileRoute('/_nav/tags/$id')({
7
+ component: tags_route_2.Component as any,
8
+ })
@@ -0,0 +1,8 @@
1
+ // AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
2
+ import { createFileRoute } from '@tanstack/react-router'
3
+ import type { PluginRouteContext } from '@allbluecn/kernel'
4
+ import { tags_route_1 } from '@allbluecn/plugins-core/tags/routes'
5
+
6
+ export const Route = createFileRoute('/_nav/tags/')({
7
+ component: tags_route_1.Component as any,
8
+ })
@@ -0,0 +1,8 @@
1
+ // AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
2
+ import { createFileRoute } from '@tanstack/react-router'
3
+ import type { PluginRouteContext } from '@allbluecn/kernel'
4
+ import { tags_route_0 } from '@allbluecn/plugins-core/tags/routes'
5
+
6
+ export const Route = createFileRoute('/_nav/tags')({
7
+ component: tags_route_0.Component as any,
8
+ })
@@ -0,0 +1,9 @@
1
+ // AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
2
+ import { createFileRoute } from '@tanstack/react-router'
3
+ import type { PluginRouteContext } from '@allbluecn/kernel'
4
+ import { tags_route_3 } from '@allbluecn/plugins-core/tags/routes'
5
+
6
+ export const Route = createFileRoute('/_nav/tags/settings')({
7
+ loader: ((ctx: PluginRouteContext) => tags_route_3.loader(ctx)) as any,
8
+ component: tags_route_3.Component as any,
9
+ })
@@ -0,0 +1,60 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { createFileRoute } from "@tanstack/react-router";
19
+ import { prisma } from "@allbluecn/database";
20
+ import { requireAuth } from "@/server/auth-helpers";
21
+
22
+ // URL 路径中的文件名不可信,Content-Disposition 用 RFC 5987 filename* 编码
23
+ function contentDisposition(fileName: string): string {
24
+ const encoded = encodeURIComponent(fileName);
25
+ return `inline; filename*=UTF-8''${encoded}`;
26
+ }
27
+
28
+ export const Route = createFileRoute("/api/attachments/$id/content")({
29
+ server: {
30
+ handlers: {
31
+ GET: async ({ request, params }) => {
32
+ try {
33
+ await requireAuth(request);
34
+
35
+ const attachment = await prisma.storyAttachment.findUnique({
36
+ where: { id: params.id },
37
+ select: { fileName: true, mimeType: true, bytes: true },
38
+ });
39
+ if (!attachment || !attachment.bytes) {
40
+ return new Response("Not found", { status: 404 });
41
+ }
42
+
43
+ const body = new Uint8Array(attachment.bytes);
44
+ return new Response(body, {
45
+ headers: {
46
+ "Content-Type": attachment.mimeType || "application/octet-stream",
47
+ "Content-Length": String(body.byteLength),
48
+ "Content-Disposition": contentDisposition(attachment.fileName),
49
+ "Cache-Control": "private, max-age=31536000, immutable",
50
+ },
51
+ });
52
+ } catch (e) {
53
+ const message = e instanceof Error ? e.message : "读取失败";
54
+ const status = message === "UNAUTHORIZED" ? 401 : 500;
55
+ return new Response(message, { status });
56
+ }
57
+ },
58
+ },
59
+ },
60
+ });
@@ -0,0 +1,148 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { createFileRoute } from "@tanstack/react-router";
19
+ import { prisma } from "@allbluecn/database";
20
+ import { requireAuth, requireUserInDb } from "@/server/auth-helpers";
21
+ import { ACTIVITY_SELECT, toActivityItem } from "@/server/serverFns/story/types";
22
+ import { inferMimeType } from "@/components/story/relations/format";
23
+
24
+ const MAX_FILE_SIZE = 10 * 1024 * 1024;
25
+
26
+ export const Route = createFileRoute("/api/attachments/upload")({
27
+ server: {
28
+ handlers: {
29
+ POST: async ({ request }) => {
30
+ try {
31
+ const session = await requireAuth(request);
32
+ const userId = session.user!.id!;
33
+ await requireUserInDb(userId);
34
+
35
+ const formData = await request.formData();
36
+ const storyId = formData.get("storyId");
37
+ const file = formData.get("file");
38
+ const rawDescription = formData.get("description");
39
+ const description =
40
+ typeof rawDescription === "string" && rawDescription.trim()
41
+ ? rawDescription.trim().slice(0, 500)
42
+ : null;
43
+ const attachmentId = formData.get("attachmentId") as string | null;
44
+ const fileHash = formData.get("fileHash") as string | null;
45
+ if (typeof storyId !== "string" || !storyId) {
46
+ return Response.json({ error: "缺少 storyId" }, { status: 400 });
47
+ }
48
+ if (!(file instanceof File)) {
49
+ return Response.json({ error: "缺少文件" }, { status: 400 });
50
+ }
51
+ if (file.size <= 0 || file.size > MAX_FILE_SIZE) {
52
+ return Response.json({ error: "文件大小需在 10MB 以内" }, { status: 400 });
53
+ }
54
+
55
+ const story = await prisma.story.findUnique({
56
+ where: { id: storyId },
57
+ select: { id: true },
58
+ });
59
+ if (!story) {
60
+ return Response.json({ error: "需求不存在" }, { status: 404 });
61
+ }
62
+
63
+ const bytes = new Uint8Array(await file.arrayBuffer());
64
+ let attachment;
65
+ let activity;
66
+
67
+ if (attachmentId) {
68
+ // 编辑模式:替换现有附件
69
+ attachment = await prisma.storyAttachment.update({
70
+ where: { id: attachmentId },
71
+ data: {
72
+ fileName: file.name.slice(0, 255),
73
+ fileSize: BigInt(file.size),
74
+ mimeType: inferMimeType(file.name, file.type),
75
+ description,
76
+ bytes,
77
+ fileHash: fileHash ?? null,
78
+ },
79
+ });
80
+ activity = await prisma.storyActivity.create({
81
+ data: {
82
+ storyId,
83
+ userId,
84
+ action: "attachment_updated",
85
+ newValue: file.name.slice(0, 255),
86
+ },
87
+ select: ACTIVITY_SELECT,
88
+ });
89
+ } else {
90
+ // 新增模式:先检查同 story 是否已有相同 hash 的附件
91
+ const existing = fileHash
92
+ ? await prisma.storyAttachment.findFirst({
93
+ where: { storyId, fileHash },
94
+ select: { id: true },
95
+ })
96
+ : null;
97
+ if (existing) {
98
+ return Response.json({ duplicated: true, existingId: existing.id }, { status: 200 });
99
+ }
100
+ attachment = await prisma.storyAttachment.create({
101
+ data: {
102
+ storyId,
103
+ fileName: file.name.slice(0, 255),
104
+ fileSize: BigInt(file.size),
105
+ mimeType: inferMimeType(file.name, file.type),
106
+ description,
107
+ storageUrl: "pending",
108
+ bytes,
109
+ fileHash: fileHash ?? null,
110
+ },
111
+ });
112
+ const storageUrl = `/api/attachments/${attachment.id}/content`;
113
+ await prisma.storyAttachment.update({
114
+ where: { id: attachment.id },
115
+ data: { storageUrl },
116
+ });
117
+ activity = await prisma.storyActivity.create({
118
+ data: {
119
+ storyId,
120
+ userId,
121
+ action: "attachment_added",
122
+ newValue: file.name.slice(0, 255),
123
+ },
124
+ select: ACTIVITY_SELECT,
125
+ });
126
+ }
127
+
128
+ const storageUrl = `/api/attachments/${attachment.id}/content`;
129
+
130
+ return Response.json({
131
+ id: attachment.id,
132
+ fileName: attachment.fileName,
133
+ fileSize: Number(attachment.fileSize),
134
+ mimeType: attachment.mimeType,
135
+ description: attachment.description,
136
+ storageUrl,
137
+ fileHash: attachment.fileHash,
138
+ activity: toActivityItem(activity),
139
+ });
140
+ } catch (e) {
141
+ const message = e instanceof Error ? e.message : "上传失败";
142
+ const status = message === "UNAUTHORIZED" ? 401 : 500;
143
+ return Response.json({ error: message }, { status });
144
+ }
145
+ },
146
+ },
147
+ },
148
+ });
@@ -0,0 +1,10 @@
1
+ // AUTO-GENERATED by @allbluecn/kernel — DO NOT EDIT
2
+ import { createFileRoute } from '@tanstack/react-router'
3
+ import type { PluginRouteContext } from '@allbluecn/kernel'
4
+ import { prd_route_3 } from '@allbluecn/plugins-core/prd/routes'
5
+
6
+ export const Route = createFileRoute('/share/prd/$prdId')({
7
+ beforeLoad: ((ctx: PluginRouteContext) => prd_route_3.beforeLoad(ctx)) as any,
8
+ validateSearch: prd_route_3.validateSearch as any,
9
+ component: prd_route_3.Component as any,
10
+ })