diff --git a/App.tsx b/App.tsx index ac47945..09652aa 100644 --- a/App.tsx +++ b/App.tsx @@ -4,7 +4,7 @@ import Layout from './components/Layout'; import Home from './views/Home'; import Write from './views/Write'; import QuestionDetail from './views/QuestionDetail'; -import DarkMatter from './views/DarkMatter'; +import WanderingPlanet from './views/WanderingPlanet'; import Privacy from './views/Privacy'; import About from './views/About'; import { AppProvider } from './contexts/AppContext'; @@ -21,7 +21,7 @@ const AnimatedRoutes: React.FC = () => { } /> } /> } /> - } /> + } /> } /> } /> diff --git a/README.md b/README.md index 1caddc6..cd9a473 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,92 @@ -1| # Cognitive Space -2| -3| Cognitive Space is a web application designed to help users organize and explore their thoughts, questions, and ideas. It allows you to drop unstructured thoughts, promote them into structured questions, and build context with claims, evidence, and triggers. Visualize connections between questions to uncover emerging patterns. -4| -5| Built with React for the frontend and deployed as a Cloudflare Worker for the backend API. -6| -7| ## Features -8| -9| - **Thought Capture**: Quickly jot down ideas without structure. -10| - **Question Organization**: Promote thoughts into questions and add supporting elements. -11| - **Visualization**: Explore relationships between questions graphically. -12| - **Dark Matter Recovery**: Rescue orphaned thoughts and link them back. -13| - **PWA Support**: Installable as a progressive web app. -14| -15| ## Guide -16| -17| 1. Open the app and use `Just write` to drop a thought without worrying about structure. -18| 2. Create a new Question by tagging or promoting a thought when the theme becomes clear. -19| 3. Add supporting notes as Claims, Evidence, or Triggers to build context over time. -20| 4. Use `Visualize` to explore related questions and spot emerging clusters. -21| 5. Visit `Dark Matter` to rescue orphaned fragments and link them to a question. -22| -23| For more details, see [Product Design](docs/product_design.md) and [Visualization Proposal](docs/visualization_proposal.md). -24| -25| ## Prerequisites -26| -27| - Node.js (version 16 or higher) -28| - Cloudflare Wrangler CLI (`npm install -g wrangler`) -29| -30| ## Installation -31| -32| 1. Clone the repository: -33| ```bash -34| git clone https://github.com/mingzhangyang/cognitive-space -35| cd cognitive-space -36| ``` -37| -38| 2. Install dependencies: -39| ```bash -40| npm install -41| ``` -42| -43| ## Local Development -44| -45| 1. Set the `BIGMODEL_API_KEY` secret using Wrangler: -46| ```bash -47| wrangler secret put BIGMODEL_API_KEY -48| ``` -49| -50| 2. Run the Worker locally (builds assets first): -51| ```bash -52| npm run dev -53| ``` -54| -55| 3. Optional: Run UI-only dev server (no Worker API): -56| ```bash -57| npm run dev:ui -58| ``` -59| -60| ## Deployment -61| -62| 1. Build the static assets: -63| ```bash -64| npm run build -65| ``` -66| -67| 2. Deploy the Worker: -68| ```bash -69| npm run deploy -70| ``` -71| -72| ## Configuration -73| -74| - `BIGMODEL_API_KEY`: Required. Your API key for the AI model. -75| - `BIGMODEL_MODEL`: Optional. Defaults to `glm-4.5-flash`. Specify a different model if needed. -76| -77| ## Scripts -78| -79| - `npm run build`: Builds the static assets. -80| - `npm run dev`: Runs the Worker locally. -81| - `npm run dev:ui`: Runs the UI dev server. -82| - `npm run deploy`: Deploys to Cloudflare. -83| -84| ## Contributing -85| -86| Contributions are welcome! Please read the contributing guidelines (if any) before submitting a pull request. -87| -88| ## License -89| -90| This project is licensed under the MIT License. -91| \ No newline at end of file +# Cognitive Space + +Cognitive Space is a web application designed to help users organize and explore their thoughts, questions, and ideas. It allows you to drop unstructured thoughts, promote them into structured questions, and build context with claims, evidence, and triggers. Visualize connections between questions to uncover emerging patterns. + +Built with React for the frontend and deployed as a Cloudflare Worker for the backend API. + +## Features + +- **Thought Capture**: Quickly jot down ideas without structure. +- **Question Organization**: Promote thoughts into questions and add supporting elements. +- **Visualization**: Explore relationships between questions graphically. +- **Wandering Planet Recovery**: Rescue orphaned thoughts and link them back. +- **PWA Support**: Installable as a progressive web app. + +## Guide + +1. Open the app and use `Just write` to drop a thought without worrying about structure. +2. Create a new Question by tagging or promoting a thought when the theme becomes clear. +3. Add supporting notes as Claims, Evidence, or Triggers to build context over time. +4. Use `Visualize` to explore related questions and spot emerging clusters. +5. Visit `Wandering Planet` to rescue orphaned fragments and link them to a question. + +For more details, see [Product Design](docs/product_design.md) and [Visualization Proposal](docs/visualization_proposal.md). + +## Prerequisites + +- Node.js (version 18 or higher) +- Cloudflare Wrangler CLI (`npm install -g wrangler`) + +## Installation + +1. Clone the repository: + ```bash + git clone https://github.com/mingzhangyang/cognitive-space + cd cognitive-space + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +## Local Development + +1. Set the `BIGMODEL_API_KEY` secret using Wrangler: + ```bash + wrangler secret put BIGMODEL_API_KEY + ``` + +2. Run the Worker locally (builds assets first): + ```bash + npm run dev + ``` + +3. Optional: Run UI-only dev server (no Worker API): + ```bash + npm run dev:ui + ``` + +## Deployment + +1. Build the static assets: + ```bash + npm run build + ``` + +2. Deploy the Worker: + ```bash + npm run deploy + ``` + +## Configuration + +- `BIGMODEL_API_KEY`: Required. Your API key for the AI model. +- `BIGMODEL_MODEL`: Optional. Defaults to `glm-4.7-flashx`. Specify a different model if needed. +- `VITE_BUILD_ID`: Optional. Injected by build scripts for service worker cache versioning. Override to control the cache tag. + +## Scripts + +- `npm run build`: Builds the static assets. +- `npm run dev`: Runs the Worker locally. +- `npm run dev:ui`: Runs the UI dev server. +- `npm run preview`: Builds the assets and runs the Worker locally. +- `npm run deploy`: Deploys to Cloudflare. + +## Contributing + +Contributions are welcome! Please read the contributing guidelines (if any) before submitting a pull request. + +## License + +This project is licensed under the MIT License. diff --git a/README.zh.md b/README.zh.md index 095dcc2..f498ed6 100644 --- a/README.zh.md +++ b/README.zh.md @@ -9,7 +9,7 @@ - **想法捕捉**: 快速记录没有结构的想法。 - **问题整理**: 将想法提升为问题,并添加支持元素。 - **可视化**: 图形化地探索问题之间的关系。 -- **暗物质恢复**: 拯救孤立的想法并将它们重新链接起来。 +- **流浪天体恢复**: 拯救孤立的想法并将它们重新链接起来。 - **PWA支持**: 可安装为渐进式Web应用。 ## 使用指南 @@ -18,13 +18,13 @@ 2. 当主题明确时,通过标记或提升想法,创建一个新的问题。 3. 随着时间的推移,添加论点、证据或触发器等支持内容来构建背景。 4. 使用“Visualize”功能探索相关问题,找到新出现的聚集点。 -5. 访问“Dark Matter”版块,拯救孤立的碎片并将它们链接到一个问题。 +5. 访问“流浪天体”版块,拯救孤立的碎片并将它们链接到一个问题。 更多详情,请参考[产品设计](docs/product_design.md)和[可视化提案](docs/visualization_proposal.md)。 ## 先决条件 -- Node.js(版本16或更高) +- Node.js(版本18或更高) - Cloudflare Wrangler CLI(`npm install -g wrangler`) ## 安装 @@ -72,13 +72,15 @@ ## 配置 - `BIGMODEL_API_KEY`: 必需,AI模型的API密钥。 -- `BIGMODEL_MODEL`: 可选,默认为`glm-4.5-flash`。如需使用其它模型,请指定。 +- `BIGMODEL_MODEL`: 可选,默认为`glm-4.7-flashx`。如需使用其它模型,请指定。 +- `VITE_BUILD_ID`: 可选。构建脚本会自动注入用于 Service Worker 缓存版本控制,可手动覆盖以固定缓存标签。 ## 脚本 - `npm run build`: 构建静态资源。 - `npm run dev`: 本地运行Worker服务。 - `npm run dev:ui`: 运行UI开发服务器。 +- `npm run preview`: 构建资源并本地运行Worker服务。 - `npm run deploy`: 部署到Cloudflare。 ## 贡献 @@ -87,4 +89,4 @@ ## 许可证 -此项目基于MIT许可证授权。 \ No newline at end of file +此项目基于MIT许可证授权。 diff --git a/components/ActionIconButton.tsx b/components/ActionIconButton.tsx index b516981..02abd70 100644 --- a/components/ActionIconButton.tsx +++ b/components/ActionIconButton.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { useAppContext } from '../contexts/AppContext'; -import { CopyIcon, EditIcon, TrashIcon } from './Icons'; +import { CopyIcon, CopyToIcon, EditIcon, MoveIcon, TrashIcon } from './Icons'; import Tooltip from './Tooltip'; -type ActionKind = 'edit' | 'copy' | 'delete'; +type ActionKind = 'edit' | 'copy' | 'copy_to' | 'delete' | 'move'; type ActionIconButtonProps = { action: ActionKind; @@ -27,10 +27,20 @@ const actionConfig: Record = { edit: { labelKey: 'edit', textClassName: UNIFIED_TEXT_CLASS, Icon: EditIcon }, copy: { labelKey: 'copy_note', textClassName: UNIFIED_TEXT_CLASS, Icon: CopyIcon }, + copy_to: { labelKey: 'copy_to_question', textClassName: UNIFIED_TEXT_CLASS, Icon: CopyToIcon }, delete: { labelKey: 'delete', textClassName: UNIFIED_TEXT_CLASS, Icon: TrashIcon }, move: { labelKey: 'move_to_question', textClassName: UNIFIED_TEXT_CLASS, Icon: MoveIcon } }; diff --git a/components/Icons.tsx b/components/Icons.tsx index 4a09686..61e8978 100644 --- a/components/Icons.tsx +++ b/components/Icons.tsx @@ -80,6 +80,15 @@ export const CopyIcon = ({ className }: { className?: string }) => ( ); +export const CopyToIcon = ({ className }: { className?: string }) => ( + + + + + + +); + export const CheckIcon = ({ className }: { className?: string }) => ( diff --git a/components/MessageCenterPanel.tsx b/components/MessageCenterPanel.tsx index 5bd0387..0fec6fd 100644 --- a/components/MessageCenterPanel.tsx +++ b/components/MessageCenterPanel.tsx @@ -12,13 +12,13 @@ import { coerceConfidenceLabel } from '../utils/confidence'; const isNoteSuggestion = (message: AssistantMessage): message is Extract => message.kind === 'note_suggestion'; -const isDarkMatterReady = (message: AssistantMessage): message is Extract => - message.kind === 'dark_matter_ready'; +const isWanderingPlanetReady = (message: AssistantMessage): message is Extract => + message.kind === 'wandering_planet_ready'; const getConfidenceLabelText = (label: string, t: (key: string) => string) => { - if (label === 'likely') return t('dark_matter_ai_confidence_likely'); - if (label === 'possible') return t('dark_matter_ai_confidence_possible'); - return t('dark_matter_ai_confidence_loose'); + if (label === 'likely') return t('wandering_planet_ai_confidence_likely'); + if (label === 'possible') return t('wandering_planet_ai_confidence_possible'); + return t('wandering_planet_ai_confidence_loose'); }; const buildNoteSuggestionDetails = (payload: NoteSuggestionPayload, t: (key: string) => string) => { @@ -109,7 +109,7 @@ const MessageCenterPanel: React.FC<{ isOpen: boolean; onClose: () => void }> = ( {runningJobs.map((job) => { const jobLabel = job.kind === 'note_analysis' ? translate('assistant_job_note') - : formatTemplate(translate('assistant_job_dark_matter'), { count: job.meta?.noteCount ?? 0 }); + : formatTemplate(translate('assistant_job_wandering_planet'), { count: job.meta?.noteCount ?? 0 }); return (
@@ -184,26 +184,26 @@ const MessageCenterPanel: React.FC<{ isOpen: boolean; onClose: () => void }> = ( ); } - if (isDarkMatterReady(message)) { + if (isWanderingPlanetReady(message)) { const count = message.payload.suggestionCount; return (
-

{translate('assistant_suggestion_dark_matter')}

+

{translate('assistant_suggestion_wandering_planet')}

- {formatTemplate(translate('assistant_dark_matter_ready_title'), { count })} + {formatTemplate(translate('assistant_wandering_planet_ready_title'), { count })}

+ ); +}; + +export default HomeRecallPanel; diff --git a/components/layout/LayoutHeader.tsx b/components/layout/LayoutHeader.tsx index 55d7f74..c2c1bbe 100644 --- a/components/layout/LayoutHeader.tsx +++ b/components/layout/LayoutHeader.tsx @@ -1,19 +1,12 @@ import React from 'react'; import { Link, useNavigate } from 'react-router-dom'; -import { - DatabaseIcon, - HomeIcon, - InboxIcon, - InfoIcon, - MenuIcon, - MoonIcon, - SunIcon -} from '../Icons'; +import { HomeIcon, InboxIcon, MenuIcon } from '../Icons'; import IconButton from '../IconButton'; import Tooltip from '../Tooltip'; import { useAppContext } from '../../contexts/AppContext'; import { useAssistantInbox } from '../../contexts/AssistantInboxContext'; import { useMenuFocus } from '../../hooks/useMenuFocus'; +import LayoutMenu from './LayoutMenu'; interface LayoutHeaderProps { isHome: boolean; @@ -51,7 +44,25 @@ const LayoutHeader: React.FC = ({ ? 'bg-accent dark:bg-accent-dark' : 'bg-line dark:bg-line-dark'; - const aboutIndex = isHome ? 3 : 2; + const handleToggleLanguage = () => { + setLanguage(language === 'en' ? 'zh' : 'en'); + setMenuOpen(false); + }; + + const handleToggleTheme = () => { + toggleTheme(); + setMenuOpen(false); + }; + + const handleOpenDataMenu = () => { + setMenuOpen(false); + onOpenDataMenu(); + }; + + const handleNavigateAbout = () => { + setMenuOpen(false); + navigate('/about'); + }; return (
@@ -98,135 +109,21 @@ const LayoutHeader: React.FC = ({ > - {menuOpen && ( - - )} +
diff --git a/components/layout/LayoutMenu.tsx b/components/layout/LayoutMenu.tsx new file mode 100644 index 0000000..783de64 --- /dev/null +++ b/components/layout/LayoutMenu.tsx @@ -0,0 +1,166 @@ +import React from 'react'; +import { DatabaseIcon, InfoIcon, MoonIcon, SunIcon } from '../Icons'; +import { Language, Theme } from '../../contexts/AppContext'; + +interface LayoutMenuProps { + isOpen: boolean; + isHome: boolean; + language: Language; + theme: Theme; + activeIndex: number; + setActiveIndex: (index: number) => void; + menuItemRefs: React.MutableRefObject>; + onKeyDown: (event: React.KeyboardEvent) => void; + onToggleLanguage: () => void; + onToggleTheme: () => void; + onOpenDataMenu: () => void; + onNavigateAbout: () => void; + t: (key: string) => string; +} + +interface LayoutMenuItemProps { + index: number; + activeIndex: number; + setActiveIndex: (index: number) => void; + menuItemRefs: React.MutableRefObject>; + onClick: () => void; + icon: React.ReactNode; + title: string; + subtitle?: string; + rightLabel?: string; +} + +const LayoutMenuItem: React.FC = ({ + index, + activeIndex, + setActiveIndex, + menuItemRefs, + onClick, + icon, + title, + subtitle, + rightLabel +}) => { + return ( + + ); +}; + +const LayoutMenu: React.FC = ({ + isOpen, + isHome, + language, + theme, + activeIndex, + setActiveIndex, + menuItemRefs, + onKeyDown, + onToggleLanguage, + onToggleTheme, + onOpenDataMenu, + onNavigateAbout, + t +}) => { + if (!isOpen) return null; + + const aboutIndex = isHome ? 3 : 2; + + return ( + + ); +}; + +export default LayoutMenu; diff --git a/components/questionDetail/QuestionDetailDialogs.tsx b/components/questionDetail/QuestionDetailDialogs.tsx new file mode 100644 index 0000000..2b194fa --- /dev/null +++ b/components/questionDetail/QuestionDetailDialogs.tsx @@ -0,0 +1,128 @@ +import React from 'react'; +import ConfirmDialog from '../ConfirmDialog'; +import MoveToQuestionModal from '../MoveToQuestionModal'; +import QuestionDowngradeDialog from '../QuestionDowngradeDialog'; + +interface RelinkTarget { + id: string; + title: string; +} + +interface QuestionDetailDialogsProps { + t: (key: string) => string; + deleteTarget: string | null; + onConfirmDelete: () => void; + onCancelDelete: () => void; + isDowngradeOpen: boolean; + relatedCount: number; + selectedType: string; + typeOptions: Array<{ id: string; label: string }>; + destination: string; + relinkTargets: RelinkTarget[]; + relinkQuestionId: string | null; + isDowngrading: boolean; + onSelectType: (type: string) => void; + onDestinationChange: (destination: string) => void; + onSelectRelinkQuestion: (id: string) => void; + onConfirmDowngrade: () => void; + onCancelDowngrade: () => void; + isMoveOpen: boolean; + isMoving: boolean; + questions: RelinkTarget[]; + selectedQuestionId: string | null; + onSelectQuestion: (id: string) => void; + onConfirmMove: () => void; + onCancelMove: () => void; + isCopyOpen: boolean; + isCopying: boolean; + copyQuestions: RelinkTarget[]; + selectedCopyQuestionId: string | null; + onSelectCopyQuestion: (id: string) => void; + onConfirmCopy: () => void; + onCancelCopy: () => void; +} + +const QuestionDetailDialogs: React.FC = ({ + t, + deleteTarget, + onConfirmDelete, + onCancelDelete, + isDowngradeOpen, + relatedCount, + selectedType, + typeOptions, + destination, + relinkTargets, + relinkQuestionId, + isDowngrading, + onSelectType, + onDestinationChange, + onSelectRelinkQuestion, + onConfirmDowngrade, + onCancelDowngrade, + isMoveOpen, + isMoving, + questions, + selectedQuestionId, + onSelectQuestion, + onConfirmMove, + onCancelMove, + isCopyOpen, + isCopying, + copyQuestions, + selectedCopyQuestionId, + onSelectCopyQuestion, + onConfirmCopy, + onCancelCopy +}) => { + return ( + <> + + + + + + ); +}; + +export default QuestionDetailDialogs; diff --git a/components/questionDetail/QuestionDetailHeader.tsx b/components/questionDetail/QuestionDetailHeader.tsx new file mode 100644 index 0000000..4df0c28 --- /dev/null +++ b/components/questionDetail/QuestionDetailHeader.tsx @@ -0,0 +1,155 @@ +import React from 'react'; +import { Note } from '../../types'; +import { ArrowDownIcon, CheckIcon, EyeIcon, EyeOffIcon, LoadingSpinner, XIcon } from '../Icons'; +import ActionIconButton from '../ActionIconButton'; +import Tooltip from '../Tooltip'; + +interface QuestionDetailHeaderProps { + t: (key: string) => string; + question: Note; + visualizationOpen: boolean; + onToggleVisualization: () => void; + onOpenDowngrade: () => void; + isSavingEdit: boolean; + editingId: string | null; + isDowngrading: boolean; + editContent: string; + onEditContentChange: (value: string) => void; + onSaveEdit: () => void; + onCancelEdit: () => void; + onEditQuestion: () => void; + onCopyQuestion: () => void; + onDeleteQuestion: () => void; +} + +const QuestionDetailHeader: React.FC = ({ + t, + question, + visualizationOpen, + onToggleVisualization, + onOpenDowngrade, + isSavingEdit, + editingId, + isDowngrading, + editContent, + onEditContentChange, + onSaveEdit, + onCancelEdit, + onEditQuestion, + onCopyQuestion, + onDeleteQuestion +}) => { + const isEditingQuestion = editingId === question.id; + + return ( +
+
+ + {t('current_problem')} + +
+ + + + + + + {!isEditingQuestion && ( + <> + + + + )} + +
+
+ {isEditingQuestion ? ( +
+