diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..cbf6585 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +# TreeAI funding link shown in the repository Sponsor button. +custom: "https://ifdian.net/a/anionex" diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..02ec098 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,76 @@ +name: Bug report +description: Report a reproducible problem in TreeAI. +title: "[Bug] " +labels: [bug] +body: + - type: markdown + attributes: + value: | + Thanks for helping improve TreeAI. Remove API keys, Authorization headers, private conversations, and sensitive screenshots before submitting. + - type: dropdown + id: area + attributes: + label: Affected area + options: + - Conversation canvas or layout + - Chat node editing or streaming + - Model configuration or API compatibility + - Session persistence or browser storage + - TXT / Markdown import + - FreeMind export + - Documentation or setup + - Other + validations: + required: true + - type: input + id: environment + attributes: + label: Environment + description: Include operating system, browser version, and Node.js version if running locally. + placeholder: macOS 15, Chrome 140, Node.js 20.19 + validations: + required: true + - type: input + id: provider + attributes: + label: Provider and model + description: Name the provider and model only. Never include the API key. + placeholder: OpenAI-compatible provider, model-name + - type: textarea + id: steps + attributes: + label: Reproduction steps + placeholder: | + 1. Configure ... + 2. Create ... + 3. Observe ... + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + validations: + required: true + - type: textarea + id: logs + attributes: + label: Sanitized logs or screenshots + description: Remove credentials, request headers, private conversation content, and unrelated personal data. + - type: checkboxes + id: checks + attributes: + label: Pre-submission checks + options: + - label: I searched existing issues and pull requests for the same problem. + required: true + - label: I removed credentials and private data from everything included above. + required: true + - label: I reviewed the compatibility and support documentation. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..50b09ab --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Support guide + url: https://github.com/Anionex/treeAI/blob/master/SUPPORT.md + about: Check setup, CORS, endpoint, streaming, storage, and file-import guidance first. + - name: Security policy + url: https://github.com/Anionex/treeAI/security/policy + about: Report suspected vulnerabilities privately instead of opening a public issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..27671fb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,37 @@ +name: Feature request +description: Propose a focused improvement that fits TreeAI's scope. +title: "[Feature] " +labels: [enhancement] +body: + - type: textarea + id: problem + attributes: + label: Problem + description: What workflow is difficult or impossible today? + validations: + required: true + - type: textarea + id: outcome + attributes: + label: Desired outcome + description: Describe the user-visible result rather than only an implementation. + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed approach + description: Include interface, persistence, protocol, or compatibility implications when known. + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + - type: checkboxes + id: scope + attributes: + label: Scope checks + options: + - label: I searched existing issues and pull requests for related proposals. + required: true + - label: I described any effect on credentials, local storage, or provider compatibility. + required: true diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 0000000..9909ba1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,37 @@ +name: Setup or usage question +description: Ask a focused question after checking the support guide. +title: "[Question] " +labels: [question] +body: + - type: markdown + attributes: + value: | + Do not include API keys, Authorization headers, private conversation text, or sensitive screenshots. + - type: textarea + id: goal + attributes: + label: What are you trying to do? + validations: + required: true + - type: textarea + id: attempted + attributes: + label: What have you tried? + description: Include sanitized commands, browser/provider details, and the relevant support section. + validations: + required: true + - type: textarea + id: result + attributes: + label: What happened? + validations: + required: true + - type: checkboxes + id: checks + attributes: + label: Checks + options: + - label: I read SUPPORT.md and the README compatibility section. + required: true + - label: I removed credentials and private data. + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..f0695d8 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ +## Summary + +- What user problem does this change solve? +- What approach did you take? + +## Verification + +- [ ] `npm ci` +- [ ] `npm run lint` +- [ ] `npm run build` +- [ ] I exercised the affected UI or API flow. +- [ ] I included a sanitized screenshot for visual changes. + +## Safety and Compatibility + +- [ ] No API keys, private conversations, Authorization headers, or sensitive browser data are included. +- [ ] Existing IndexedDB data remains compatible, or a migration is included and documented. +- [ ] README, limitations, and changelog are updated for user-visible behavior. +- [ ] The pull request is focused and contains no unrelated refactor. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4f1ec20 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Build + run: npm run build diff --git a/.gitignore b/.gitignore index 7ceb59f..5a1f241 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ lerna-debug.log* node_modules dist dist-ssr +.playwright-cli *.local # Editor directories and files diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d214860 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +All user-visible changes to TreeAI are documented here. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses semantic versioning for tagged releases. + +## [Unreleased] + +### Added + +- Bilingual English and Chinese project documentation. +- Repository-owned hero, product screenshot, and social-preview assets. +- MIT license, funding guide, security policy, support guide, contribution guide, issue forms, pull-request template, and CI workflow. +- Browser metadata and a TreeAI favicon. + +### Changed + +- Unified the visible product name as TreeAI. +- Documented the client-only data boundary, provider compatibility, credential storage, and current limitations. +- Limited file import to the TXT and Markdown formats that are actually implemented. +- Removed unused PDF and DOCX dependencies. +- Cleaned up TypeScript and React code so ESLint passes. + +## [0.1.0] - 2025-05-13 + +### Added + +- Initial public preview of the React Flow conversation-tree workspace. +- Local session and model persistence with Dexie and IndexedDB. +- OpenAI-compatible streaming chat requests and FreeMind export. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f1f4053 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,55 @@ +# TreeAI Code of Conduct + +## Our Pledge + +We as members, contributors, and maintainers pledge to make participation in the TreeAI community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment include: + +- demonstrating empathy and kindness toward other people; +- respecting differing opinions, viewpoints, and experiences; +- giving and gracefully accepting constructive feedback; +- accepting responsibility, apologizing to those affected by mistakes, and learning from the experience; +- focusing on what is best for the community and the project. + +Examples of unacceptable behavior include: + +- sexualized language or imagery and sexual attention or advances of any kind; +- trolling, insulting or derogatory comments, and personal or political attacks; +- public or private harassment; +- publishing another person's private information without explicit permission; +- sharing credentials, private conversations, or sensitive screenshots that do not belong to you; +- other conduct that could reasonably be considered inappropriate in a professional setting. + +## Enforcement Responsibilities + +Community maintainers are responsible for clarifying and enforcing standards of acceptable behavior. They may remove, edit, or reject comments, commits, code, issues, and other contributions that do not align with this Code of Conduct, and will communicate moderation reasons when appropriate. + +## Scope + +This Code of Conduct applies within all project spaces and when an individual officially represents the project in public spaces. + +## Reporting + +Report abusive, harassing, or otherwise unacceptable behavior privately to `davidyang042@gmail.com` with the subject `TreeAI conduct report`. Do not report conduct incidents through a public issue. + +Reports will be reviewed and investigated as promptly and fairly as practical. Maintainers will respect the privacy and security of reporters and affected participants whenever possible. + +## Enforcement Guidelines + +Maintainers may use the following consequences based on the impact and pattern of behavior: + +1. **Correction** - A private explanation of the violation and how to correct it. +2. **Warning** - A warning with consequences for continued behavior. +3. **Temporary restriction** - Time-limited restrictions on project participation or communication. +4. **Permanent ban** - Permanent removal from project spaces for sustained or severe violations. + +The response may skip steps when necessary to protect the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1, permanently available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html). The enforcement guidelines draw on Mozilla's code-of-conduct enforcement ladder. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6e131c6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# Contributing to TreeAI + +Thanks for helping improve TreeAI. Focused bug fixes, compatibility work, tests, accessibility improvements, documentation, and carefully scoped features are welcome. + +By participating, you agree to follow the [Code of Conduct](CODE_OF_CONDUCT.md). + +## Before You Start + +1. Read [README.md](README.md), especially compatibility, data boundaries, and limitations. +2. Search existing issues and pull requests before starting duplicate work. +3. Open an issue before broad architecture changes, persistence migrations, protocol changes, or new collaboration features. +4. Never include API keys, private conversations, Authorization headers, or unsanitized browser storage in an issue or pull request. + +## Local Setup + +Use Node.js 18 or newer: + +```bash +git clone https://github.com/Anionex/treeAI.git +cd treeAI +npm ci +npm run dev +``` + +## Project Boundaries + +- TreeAI is currently a client-only application. Discuss any backend, account, telemetry, or sync proposal before implementing it. +- Keep the browser-to-provider request boundary explicit. Do not proxy, log, or persist credentials in a new location without a security review. +- The IndexedDB database is still named `TreeChatDatabase` for compatibility with existing local data. Do not rename it without a versioned migration. +- Keep OpenAI-compatible streaming behavior backward compatible unless a change is explicitly scoped as a protocol migration. +- Publish only source-confirmed features. Partially implemented work belongs in project status or limitations. +- Keep pull requests narrow; do not combine unrelated refactors with a fix. + +## Verification + +Run all checks before opening a pull request: + +```bash +npm ci +npm run lint +npm run build +``` + +For user-interface changes, also run the app, exercise the affected flow, and include a sanitized screenshot when visual behavior changes. + +## Pull Requests + +- Explain the user problem and the chosen solution. +- Link the relevant issue when one exists. +- Describe behavior changes and compatibility risks. +- Add or update tests when a practical test surface exists. +- Update README, changelog, screenshots, or limitations when user-facing behavior changes. +- Keep generated lockfile changes limited to dependency changes made by the pull request. + +## Reporting Security Issues + +Do not open a public issue for a suspected vulnerability. Follow [SECURITY.md](SECURITY.md). diff --git a/FUNDING.md b/FUNDING.md new file mode 100644 index 0000000..5f33c10 --- /dev/null +++ b/FUNDING.md @@ -0,0 +1,31 @@ +# Funding / 赞助 + +## English + +TreeAI is open source under the MIT License. If it helps you explore and compare LLM conversations, you can support continued development through [Afdian](https://ifdian.net/a/anionex). + +Sponsorship helps fund: + +- browser and provider compatibility testing; +- visual workflow, accessibility, and performance improvements; +- test coverage and long-term maintenance; +- documentation, examples, and reusable conversation-tree workflows. + +Financial support does not purchase roadmap control, privileged security handling, or private access to user data. Bugs, feature proposals, and contributions continue to follow the same public project process. + +Non-financial support also helps: star or share the repository, report reproducible issues, improve documentation, or contribute focused fixes. + +## 中文 + +TreeAI 以 MIT License 开源。如果它帮助你更清晰地探索和比较大模型对话,可以通过[爱发电](https://ifdian.net/a/anionex)支持持续开发。 + +赞助主要用于: + +- 浏览器与不同模型服务商的兼容性验证; +- 可视化工作流、无障碍与性能优化; +- 自动化测试和长期维护; +- 文档、示例与可复用的树状对话工作流。 + +赞助不会换取路线图控制权、安全例外或用户数据访问权。Bug、功能建议和代码贡献仍按项目公开流程处理。 + +非资金支持同样重要:Star 或分享仓库、提交可复现的问题、改进文档,以及贡献聚焦的修复。 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4e0a3c8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025-2026 Anionex + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 787964e..dba0c84 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,160 @@ -# TreeChat - 树状结构对话应用 +

+ TreeAI - a visual workspace for branching LLM conversations +

-## 项目描述 -TreeChat是一个基于树状结构的对话应用,支持文件上传、多模型管理和会话管理。采用React + TypeScript构建,提供直观的聊天界面和系统交互功能。 +
-# Usage -1. 确保已安装Node.js 18+ -2. 克隆项目仓库 -3. 安装依赖 +# TreeAI -``` -npm install +[![GitHub stars](https://img.shields.io/github/stars/Anionex/treeAI?style=flat-square&logo=github)](https://github.com/Anionex/treeAI/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/Anionex/treeAI?style=flat-square&logo=github)](https://github.com/Anionex/treeAI/forks) +[![CI](https://img.shields.io/github/actions/workflow/status/Anionex/treeAI/ci.yml?branch=master&style=flat-square&label=CI)](https://github.com/Anionex/treeAI/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/github/license/Anionex/treeAI?style=flat-square&color=2F7A5A)](LICENSE) + +[![React](https://img.shields.io/badge/React-18-61DAFB?style=flat-square&logo=react&logoColor=0B172A)](package.json) +[![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?style=flat-square&logo=typescript&logoColor=white)](src/) + +**Branch the conversation. Keep every line of thought.** + +A local-first visual workspace for exploring, comparing, and exporting LLM conversation trees across OpenAI-compatible models. + +🌐 [**中文**](README_CN.md) | **English** + +
+ +Linear chat forces every follow-up into one timeline: explore a different assumption and the original path is pushed out of view. TreeAI turns the conversation into a canvas. Continue from any node, create parallel branches, switch model settings per branch, and preserve every route for later comparison. + +All sessions and model configurations stay in the browser's IndexedDB. Requests go directly from the browser to the API endpoint you configure; TreeAI has no application backend. + +

+ TreeAI canvas showing one system prompt branching into two independent replies, with a third follow-up continuing from the left branch +

+ +

One prompt, multiple independent paths. Continue any branch without overwriting the others.

+ +## Why TreeAI + +| Capability | What it gives you | +|---|---| +| **Branch from any node** | Explore an alternative question, assumption, or answer while preserving the original conversation path. | +| **Tune each branch independently** | Select the model, temperature, and token budget at the node level instead of locking one configuration to the whole session. | +| **Keep the workspace local** | Store sessions and model profiles in browser IndexedDB with no TreeAI server in the middle. | +| **Bring in text context** | Add `.txt` and `.md` files as a new conversation node. | +| **Export the tree** | Download a FreeMind-compatible `.mm` file for archiving or further mind-map work. | + +## Quick Start + +### Prerequisites + +- Node.js 18 or newer +- npm +- An OpenAI-compatible API that supports streaming `POST /chat/completions` +- A provider that allows browser CORS requests from your TreeAI origin + +### Run locally + +```bash +git clone https://github.com/Anionex/treeAI.git +cd treeAI +npm ci npm run dev ``` -minimal theme(new): -image +Open the local URL printed by Vite, usually `http://localhost:5173`. +### First conversation +1. Open **Model settings** from the gear button in the lower-left corner. +2. Add a model name, an API base URL such as `https://api.openai.com/v1`, an API key, and the provider's model identifier. +3. Create a conversation and edit the system-prompt node. +4. Add a child node, enter a message, and send it. +5. Use the **+** button on any node to continue that branch or start a parallel one. -## 技术栈 -- 前端框架: React 18 -- 语言: TypeScript -- 构建工具: Vite -- CSS框架: Tailwind CSS + PostCSS -- 状态管理: 自定义Store方案 -- 代码规范: ESLint +> [!IMPORTANT] +> API keys are stored unencrypted in this browser's IndexedDB because TreeAI is a client-only application. Use restricted keys or a trusted local proxy, and do not configure secrets on a shared or untrusted device. -## 功能特性 -- 树状结构对话流 (ChatFlow) -- 文件上传处理 (FileUploadButton) -- 多AI模型管理 (ModelManager) -- 侧边栏导航 (Sidebar) -- 会话状态管理 (sessionStore) -- 数据库上下文 (DatabaseContext) -- 实用工具集 (fileUtils, exportUtils) +## How It Works - -## 项目结构 +```mermaid +flowchart LR + UI["React + React Flow canvas"] --> Stores["Session and model stores"] + Stores <--> DB["Dexie / IndexedDB"] + UI --> API["Streaming chat service"] + API --> Provider["OpenAI-compatible API"] + Files["TXT / Markdown files"] --> UI + Stores --> Export["FreeMind .mm export"] ``` + +- **Conversation graph:** each chat node stores a `parentId`, allowing the UI to reconstruct and render independent branches. +- **Context assembly:** when a node is sent, TreeAI walks its ancestors and sends only that branch's message chain. +- **Streaming:** responses are consumed from Server-Sent Events and rendered incrementally in the active node. +- **Persistence:** sessions, node positions, and model profiles are stored locally through Dexie. + +## Compatibility and Data Boundaries + +TreeAI currently targets OpenAI-style streaming Chat Completions APIs. A compatible provider must: + +- expose `POST {baseUrl}/chat/completions`; +- accept `Authorization: Bearer ...`; +- return `data:` Server-Sent Events with OpenAI-like `choices[0].delta.content`, or one of the small fallback text shapes handled in `apiService.ts`; +- allow direct browser requests through CORS. + +TreeAI sends the configured API key directly to that provider. The repository does not include a relay server, analytics service, or cloud account system. + +## Project Status and Limitations + +TreeAI is an early-stage project (`0.1.0`). The core branching workflow builds and runs, but the following constraints are intentional and user-visible: + +- reasoning-model or provider-specific thinking streams are not supported; +- only `.txt` and `.md` file text extraction is enabled; +- model credentials are stored locally without encryption; +- there is no account sync, collaboration, or cross-device backup; +- automated application tests have not been added yet; CI currently verifies lint and production build; +- the production bundle is still large and would benefit from code splitting. + +See [todo.md](todo.md) and [open issues](https://github.com/Anionex/treeAI/issues) for active work. + +## Development + +| Command | Purpose | +|---|---| +| `npm run dev` | Start the Vite development server. | +| `npm run lint` | Run ESLint across the project. | +| `npm run build` | Create a production bundle. | +| `npm run preview` | Serve the production build locally. | + +The main modules are: + +```text src/ -├── App.tsx # 主应用组件 -├── main.tsx # 应用入口 -├── index.css # 全局样式 -├── types.ts # 类型定义 -├── components/ # UI组件 -│ ├── ChatFlow.tsx # 对话流组件 -│ ├── FileUploadButton.tsx # 文件上传 -│ ├── ModelManager.tsx # 模型管理 -│ ├── Sidebar.tsx # 侧边栏 -│ └── nodes/ # 对话节点组件 -├── context/ # 上下文 -│ └── DatabaseContext.tsx -├── db/ # 数据库相关 -│ └── db.ts -├── services/ # 服务层 -│ └── apiService.ts -├── stores/ # 状态存储 -│ ├── modelStore.ts -│ └── sessionStore.ts -└── utils/ # 工具函数 - ├── exportUtils.ts - └── fileUtils.ts +├── components/ # Canvas, nodes, sidebar, model manager, uploads +├── context/ # Database loading boundary +├── db/ # Dexie / IndexedDB persistence +├── services/ # Streaming OpenAI-compatible requests +├── stores/ # Session and model state +└── utils/ # File extraction, notifications, mind-map export ``` +Before opening a pull request, run: + +```bash +npm ci +npm run lint +npm run build +``` + +## Community + +- Questions and setup help: [Support guide](SUPPORT.md) +- Bugs and feature requests: [Issue forms](https://github.com/Anionex/treeAI/issues/new/choose) +- Contributions: [Contributing guide](CONTRIBUTING.md) +- Security reports: [Security policy](SECURITY.md) +- Community standards: [Code of Conduct](CODE_OF_CONDUCT.md) +- User-visible changes: [Changelog](CHANGELOG.md) +- Sponsorship: [Funding guide](FUNDING.md) + +## About + +TreeAI is maintained by [Anionex](https://github.com/Anionex). If the project helps you explore LLM conversations more clearly, you can support it by starring the repository, sharing it, reporting reproducible issues, contributing focused improvements, or [sponsoring continued development](FUNDING.md). -## 贡献 -欢迎提交Pull Request,请确保代码通过所有检查。 +Released under the [MIT License](LICENSE). diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 0000000..1065061 --- /dev/null +++ b/README_CN.md @@ -0,0 +1,160 @@ +

+ TreeAI - 用树状画布探索大模型对话 +

+ +
+ +# TreeAI + +[![GitHub stars](https://img.shields.io/github/stars/Anionex/treeAI?style=flat-square&logo=github)](https://github.com/Anionex/treeAI/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/Anionex/treeAI?style=flat-square&logo=github)](https://github.com/Anionex/treeAI/forks) +[![CI](https://img.shields.io/github/actions/workflow/status/Anionex/treeAI/ci.yml?branch=master&style=flat-square&label=CI)](https://github.com/Anionex/treeAI/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/github/license/Anionex/treeAI?style=flat-square&color=2F7A5A)](LICENSE) + +[![React](https://img.shields.io/badge/React-18-61DAFB?style=flat-square&logo=react&logoColor=0B172A)](package.json) +[![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?style=flat-square&logo=typescript&logoColor=white)](src/) + +**让对话分叉,也让每一条思路都被保留下来。** + +一个本地优先的可视化大模型对话工作区,用于探索、对比和导出兼容 OpenAI 接口的 LLM 对话树。 + +🌐 **中文** | [**English**](README.md) + +
+ +线性聊天会把所有追问都塞进同一条时间线:一旦探索另一种假设,原来的路径就很快被挤出视野。TreeAI 把对话变成一张画布。你可以从任意节点继续、创建并行分支、为不同分支切换模型参数,并把每一条路线完整保留下来进行比较。 + +所有会话和模型配置都保存在浏览器的 IndexedDB 中。请求由浏览器直接发送到你配置的 API 地址;TreeAI 本身没有应用后端。 + +

+ TreeAI 画布中,一个系统提示词分出两条独立回复,其中左侧分支继续产生了第三个追问节点 +

+ +

一个起点,多条独立路径;继续任意分支都不会覆盖其他思路。

+ +## 为什么使用 TreeAI + +| 能力 | 带来的价值 | +|---|---| +| **从任意节点分叉** | 探索另一种问题、假设或答案,同时保留原有对话路径。 | +| **每个分支独立调参** | 在节点级选择模型、温度和 token 上限,而不是让整段会话被一个配置锁死。 | +| **本地优先** | 会话和模型配置保存在浏览器 IndexedDB,中间没有 TreeAI 服务器。 | +| **导入文本上下文** | 把 `.txt` 和 `.md` 文件内容作为新的对话节点加入画布。 | +| **导出对话树** | 下载兼容 FreeMind 的 `.mm` 文件,用于归档或继续制作思维导图。 | + +## 快速开始 + +### 前置要求 + +- Node.js 18 或更高版本 +- npm +- 支持流式 `POST /chat/completions` 的 OpenAI-compatible API +- 服务商允许来自 TreeAI 页面来源的浏览器 CORS 请求 + +### 本地运行 + +```bash +git clone https://github.com/Anionex/treeAI.git +cd treeAI +npm ci +npm run dev +``` + +打开 Vite 输出的本地地址,通常是 `http://localhost:5173`。 + +### 开始第一段对话 + +1. 点击左下角齿轮,打开**模型管理**。 +2. 填写模型名称、API Base URL(例如 `https://api.openai.com/v1`)、API Key 和服务商的模型标识。 +3. 创建新会话并编辑系统提示词节点。 +4. 添加子节点,输入消息并发送。 +5. 点击任意节点上的 **+**,继续当前分支或创建并行路径。 + +> [!IMPORTANT] +> TreeAI 是纯前端应用,API Key 会以未加密形式保存在当前浏览器的 IndexedDB 中。请使用权限受限的 Key 或可信的本地代理,不要在共享或不可信设备上配置凭据。 + +## 工作原理 + +```mermaid +flowchart LR + UI["React + React Flow 对话画布"] --> Stores["会话与模型 Store"] + Stores <--> DB["Dexie / IndexedDB"] + UI --> API["流式聊天服务"] + API --> Provider["OpenAI-compatible API"] + Files["TXT / Markdown 文件"] --> UI + Stores --> Export["FreeMind .mm 导出"] +``` + +- **对话图结构:** 每个聊天节点保存 `parentId`,界面据此重建和渲染独立分支。 +- **上下文组装:** 发送某个节点时,TreeAI 会沿父节点向上回溯,只提交该分支上的消息链。 +- **流式输出:** 应用读取 Server-Sent Events,并把增量内容实时渲染到当前节点。 +- **本地持久化:** 会话、节点位置和模型配置通过 Dexie 保存在浏览器中。 + +## 兼容性与数据边界 + +TreeAI 当前面向 OpenAI 风格的流式 Chat Completions API。兼容服务商需要: + +- 提供 `POST {baseUrl}/chat/completions`; +- 接受 `Authorization: Bearer ...`; +- 返回 `data:` 形式的 Server-Sent Events,并提供 OpenAI 风格的 `choices[0].delta.content`,或 `apiService.ts` 已处理的少量兼容文本结构; +- 允许浏览器通过 CORS 直接请求。 + +TreeAI 会把配置的 API Key 直接发送给对应服务商。仓库不包含中转服务器、统计分析服务或云端账户系统。 + +## 项目状态与限制 + +TreeAI 仍处于早期阶段(`0.1.0`)。核心树状对话流程可以构建和运行,但目前存在以下明确限制: + +- 暂不支持思考模型或服务商特有的 reasoning stream; +- 文件文本提取目前只启用 `.txt` 和 `.md`; +- 模型凭据在本地保存,但没有加密; +- 暂无账户同步、多人协作和跨设备备份; +- 尚未加入自动化应用测试;CI 目前验证 ESLint 和生产构建; +- 生产 bundle 仍偏大,后续需要进行代码拆分。 + +当前工作可见 [todo.md](todo.md) 和 [GitHub Issues](https://github.com/Anionex/treeAI/issues)。 + +## 开发 + +| 命令 | 用途 | +|---|---| +| `npm run dev` | 启动 Vite 开发服务器。 | +| `npm run lint` | 对整个项目运行 ESLint。 | +| `npm run build` | 生成生产构建。 | +| `npm run preview` | 在本地预览生产构建。 | + +主要目录: + +```text +src/ +├── components/ # 画布、节点、侧栏、模型管理、文件上传 +├── context/ # 数据库加载边界 +├── db/ # Dexie / IndexedDB 持久化 +├── services/ # OpenAI-compatible 流式请求 +├── stores/ # 会话与模型状态 +└── utils/ # 文件提取、通知、思维导图导出 +``` + +提交 Pull Request 前请运行: + +```bash +npm ci +npm run lint +npm run build +``` + +## 社区 + +- 使用和配置帮助:[支持指南](SUPPORT.md) +- Bug 与功能建议:[Issue 表单](https://github.com/Anionex/treeAI/issues/new/choose) +- 参与贡献:[贡献指南](CONTRIBUTING.md) +- 安全问题:[安全策略](SECURITY.md) +- 社区规范:[行为准则](CODE_OF_CONDUCT.md) +- 用户可见变更:[更新日志](CHANGELOG.md) +- 赞助项目:[Funding 说明](FUNDING.md) + +## 关于 + +TreeAI 由 [Anionex](https://github.com/Anionex) 维护。如果它让你更清晰地探索大模型对话,欢迎 Star、分享、提交可复现的问题、贡献聚焦的改进,或通过 [Funding 说明](FUNDING.md) 支持持续开发。 + +本项目采用 [MIT License](LICENSE) 开源。 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..1b67f79 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,32 @@ +# Security Policy + +## Supported Versions + +Security fixes target the latest code on the default branch and the latest tagged release, when one exists. Older snapshots may not receive backports. + +## Reporting a Vulnerability + +Email `davidyang042@gmail.com` with the subject `TreeAI security report`. + +Include: + +- the affected version or commit; +- a clear description of the issue and impact; +- minimal reproduction steps or a proof of concept; +- relevant browser, provider, and operating-system details; +- suggested remediation, if known. + +Do not include live API keys, private conversations, unrelated personal data, or destructive payloads. Do not open a public issue until the report has been reviewed and a coordinated disclosure plan has been agreed. + +## Security Model + +TreeAI is a client-only browser application: + +- model API keys are stored unencrypted in IndexedDB; +- requests are sent directly from the browser to the configured provider; +- TreeAI does not provide a relay server, account system, or cloud backup; +- browser extensions, injected scripts, shared profiles, and compromised origins may access local data. + +Use restricted credentials, prefer a trusted local proxy when possible, and do not configure TreeAI on shared or untrusted devices. + +Useful reports include credential exposure, cross-site scripting, unsafe export handling, storage-boundary bypasses, dependency vulnerabilities with a demonstrated path, and provider-request behavior that leaks secrets. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000..da29061 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,37 @@ +# TreeAI Support + +## Start Here + +1. Confirm Node.js 18 or newer with `node --version`. +2. Run `npm ci`, then `npm run lint` and `npm run build`. +3. Review [Compatibility and Data Boundaries](README.md#compatibility-and-data-boundaries). +4. Remove secrets and private conversation content before sharing logs or screenshots. + +## Common Problems + +### The browser blocks the API request + +TreeAI calls the configured provider directly. The provider must allow browser CORS requests from the TreeAI origin. If it does not, use a trusted local proxy that adds the required CORS headers. + +### The endpoint returns 404 + +Enter the API base URL, usually ending in `/v1`, rather than the full `/chat/completions` path. TreeAI appends `/chat/completions` itself. + +### No text appears while streaming + +TreeAI expects Server-Sent Events containing `data:` lines and an OpenAI-style `choices[0].delta.content` field. Provider-specific reasoning streams and unrelated event formats are not currently supported. + +### I need to clear local data + +Sessions and model profiles live in browser IndexedDB under `TreeChatDatabase`. Use the browser's site-data or developer-tools storage controls to remove it. This permanently deletes local TreeAI data for that origin. + +### File import fails + +Only plain-text `.txt` and Markdown `.md` files are currently supported. + +## Where to Ask + +- Reproducible bugs: use the [bug report form](https://github.com/Anionex/treeAI/issues/new/choose). +- Focused feature proposals: use the feature request form. +- Setup and usage questions: use the question form. +- Security concerns: follow [SECURITY.md](SECURITY.md) instead of opening a public issue. diff --git a/assets/hero.png b/assets/hero.png new file mode 100644 index 0000000..9218ecd Binary files /dev/null and b/assets/hero.png differ diff --git a/assets/social-preview.png b/assets/social-preview.png new file mode 100644 index 0000000..7121efb Binary files /dev/null and b/assets/social-preview.png differ diff --git a/assets/treeai-workspace.png b/assets/treeai-workspace.png new file mode 100644 index 0000000..d6457c4 Binary files /dev/null and b/assets/treeai-workspace.png differ diff --git a/index.html b/index.html index 5b410b8..e5ec1ed 100644 --- a/index.html +++ b/index.html @@ -1,15 +1,19 @@ - + - + - TreeChat AI + + + TreeAI - Branch every line of thought -
- \ No newline at end of file + diff --git a/package-lock.json b/package-lock.json index ae178d9..815aebd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,20 @@ { - "name": "tree-chat-ai", + "name": "treeai", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "tree-chat-ai", + "name": "treeai", "version": "0.1.0", + "license": "MIT", "dependencies": { "dexie": "^3.2.4", "dexie-react-hooks": "^1.1.6", "gsap": "^3.12.5", "lucide-react": "^0.344.0", - "mammoth": "^1.6.0", "md-editor-rt": "^5.5.1", "md-editor-v3": "^4.12.1", - "pdf-parse": "^1.1.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", @@ -39,6 +38,9 @@ "typescript": "^5.5.3", "typescript-eslint": "^8.3.0", "vite": "^5.4.2" + }, + "engines": { + "node": ">=18" } }, "node_modules/@alloc/quick-lru": { @@ -2642,15 +2644,6 @@ "license": "MIT", "peer": true }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", - "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -2794,26 +2787,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2826,12 +2799,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", - "license": "MIT" - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3040,12 +3007,6 @@ "toggle-selection": "^1.0.6" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, "node_modules/crelt": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", @@ -3243,27 +3204,12 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true }, - "node_modules/dingbat-to-unicode": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", - "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==", - "license": "BSD-2-Clause" - }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, - "node_modules/duck": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", - "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", - "license": "BSD", - "dependencies": { - "underscore": "^1.13.1" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -3904,12 +3850,6 @@ "node": ">= 4" } }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -3935,12 +3875,6 @@ "node": ">=0.8.19" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -4007,12 +3941,6 @@ "node": ">=0.12.0" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4102,18 +4030,6 @@ "node": ">=6" } }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4136,15 +4052,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -4215,17 +4122,6 @@ "loose-envify": "cli.js" } }, - "node_modules/lop": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", - "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", - "license": "BSD-2-Clause", - "dependencies": { - "duck": "^0.1.12", - "option": "~0.2.1", - "underscore": "^1.13.1" - } - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -4254,39 +4150,6 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, - "node_modules/mammoth": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.9.0.tgz", - "integrity": "sha512-F+0NxzankQV9XSUAuVKvkdQK0GbtGGuqVnND9aVf9VSeUA82LQa29GjLqYU6Eez8LHqSJG3eGiDW3224OKdpZg==", - "license": "BSD-2-Clause", - "dependencies": { - "@xmldom/xmldom": "^0.8.6", - "argparse": "~1.0.3", - "base64-js": "^1.5.1", - "bluebird": "~3.4.0", - "dingbat-to-unicode": "^1.0.1", - "jszip": "^3.7.1", - "lop": "^0.4.2", - "path-is-absolute": "^1.0.0", - "underscore": "^1.13.1", - "xmlbuilder": "^10.0.0" - }, - "bin": { - "mammoth": "bin/mammoth" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/mammoth/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, "node_modules/markdown-it": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", @@ -4460,7 +4323,8 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true }, "node_modules/mz": { "version": "2.7.0", @@ -4497,12 +4361,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/node-ensure": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz", - "integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==", - "license": "MIT" - }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -4544,12 +4402,6 @@ "node": ">= 6" } }, - "node_modules/option": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", - "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", - "license": "BSD-2-Clause" - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4603,12 +4455,6 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4630,15 +4476,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -4676,28 +4513,6 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true }, - "node_modules/pdf-parse": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.1.tgz", - "integrity": "sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A==", - "license": "MIT", - "dependencies": { - "debug": "^3.1.0", - "node-ensure": "^0.0.0" - }, - "engines": { - "node": ">=6.8.1" - } - }, - "node_modules/pdf-parse/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4898,12 +4713,6 @@ "node": ">= 0.8.0" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5035,21 +4844,6 @@ "pify": "^2.3.0" } }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -5156,12 +4950,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -5179,12 +4967,6 @@ "semver": "bin/semver.js" } }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "license": "MIT" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5226,21 +5008,6 @@ "node": ">=0.10.0" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -5569,12 +5336,6 @@ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "license": "MIT" }, - "node_modules/underscore": { - "version": "1.13.7", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", - "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", - "license": "MIT" - }, "node_modules/update-browserslist-db": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", @@ -5626,7 +5387,8 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true }, "node_modules/uuid": { "version": "9.0.1", @@ -5873,15 +5635,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/xmlbuilder": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", - "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, "node_modules/xss": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", diff --git a/package.json b/package.json index 8a422d2..44e6752 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,28 @@ { - "name": "tree-chat-ai", + "name": "treeai", "private": true, "version": "0.1.0", + "engines": { + "node": ">=18" + }, + "description": "A local-first visual workspace for branching, comparing, and exporting LLM conversation trees.", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/Anionex/treeAI.git" + }, + "bugs": { + "url": "https://github.com/Anionex/treeAI/issues" + }, + "homepage": "https://github.com/Anionex/treeAI#readme", + "keywords": [ + "ai-chat", + "branching-conversations", + "conversation-tree", + "llm", + "local-first", + "react-flow" + ], "type": "module", "scripts": { "dev": "vite", @@ -14,10 +35,8 @@ "dexie-react-hooks": "^1.1.6", "gsap": "^3.12.5", "lucide-react": "^0.344.0", - "mammoth": "^1.6.0", "md-editor-rt": "^5.5.1", "md-editor-v3": "^4.12.1", - "pdf-parse": "^1.1.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..3f5cdb2 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/App.tsx b/src/App.tsx index e0e7017..e1cf4a7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -77,7 +77,7 @@ const App: React.FC = () => { ) : (
-

欢迎使用 TreeChat

+

欢迎使用 TreeAI

创建一个新会话,开始与 AI 进行树状结构的对话。

@@ -116,4 +116,4 @@ const App: React.FC = () => { ); }; -export default App; \ No newline at end of file +export default App; diff --git a/src/components/ChatFlow.tsx b/src/components/ChatFlow.tsx index e1cba92..6eb2d1b 100644 --- a/src/components/ChatFlow.tsx +++ b/src/components/ChatFlow.tsx @@ -14,13 +14,12 @@ import SystemNode from './nodes/SystemNode'; import ChatNode from './nodes/ChatNode'; import { useSessionStore } from '../stores/sessionStore'; import { useModelStore } from '../stores/modelStore'; -import { ChatNode as ChatNodeType, Session } from '../types'; -import { sendChatRequest, abortRequest } from '../services/apiService'; -import { Share2, UploadCloud, LayoutGrid, FileUp } from 'lucide-react'; +import { ChatNode as ChatNodeType } from '../types'; +import { sendChatRequest } from '../services/apiService'; +import { Share2, LayoutGrid, FileUp } from 'lucide-react'; import { exportToMindmap } from '../utils/exportUtils'; -import { gsap } from 'gsap'; import FileUploadButton from './FileUploadButton'; -import { showSuccess, showInfo, showWarning, showError } from '../utils/notification'; +import { showSuccess, showError } from '../utils/notification'; const nodeTypes = { system: SystemNode, @@ -32,17 +31,14 @@ interface ChatFlowProps { } const ReactFlowWrapper: React.FC = ({ sessionId }) => { - const { sessions, updateSession, addNodeToSession, updateNodeInSession, deleteNodeFromSession } = useSessionStore(); + const { sessions, addNodeToSession, updateNodeInSession, deleteNodeFromSession } = useSessionStore(); const { models, defaultModelId } = useModelStore(); const session = sessions.find(s => s.id === sessionId); const [nodes, setNodes] = useState([]); const [edges, setEdges] = useState([]); const reactFlowInstance = useReactFlow(); const abortControllerRef = useRef>({}); - const prevNodeCountRef = useRef(0); - const [nodeDimensions, setNodeDimensions] = useState>({}); - const [autoLayout, setAutoLayout] = useState(true); - const [isTyping, setIsTyping] = useState(false); + const [nodeDimensions] = useState>({}); const [streamingResponses, setStreamingResponses] = useState>({}); const calculateNodeLayout = useCallback((forceRecalculate = false) => { @@ -108,8 +104,6 @@ const ReactFlowWrapper: React.FC = ({ sessionId }) => { calculateSubtreeWidth(systemNode.id); } - const levelHeights = new Map(); - const calculateNodePosition = (nodeId: string, startX: number, level: number, startY: number) => { const nodeDim = getNodeDimensions(nodeId); const width = subtreeWidths.get(nodeId) || nodeDim.width; @@ -191,10 +185,12 @@ const ReactFlowWrapper: React.FC = ({ sessionId }) => { reactFlowInstance.fitView({ padding: 0.2 }); }, 50); + // The handlers below intentionally read the latest session state from this render. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [session, nodeDimensions, streamingResponses, sessionId, updateNodeInSession]); - const handleAddChildNode = async (parentId: string) => { + const handleAddChildNode = (parentId: string) => { if (!session || !defaultModelId) return; const parentNode = session.nodes.find(n => n.id === parentId); @@ -232,7 +228,7 @@ const ReactFlowWrapper: React.FC = ({ sessionId }) => { addNodeToSession(sessionId, newNode); }; - const handleEditNode = (nodeId: string, content: string, type: 'user' | 'assistant' | 'system', isInputting = false) => { + const handleEditNode = (nodeId: string, content: string, type: 'user' | 'assistant' | 'system') => { if (!session) return; const node = session.nodes.find(n => n.id === nodeId); @@ -353,13 +349,14 @@ const ReactFlowWrapper: React.FC = ({ sessionId }) => { return newState; }); - } catch (error: any) { + } catch (error: unknown) { console.error('Chat request failed:', error); + const message = error instanceof Error ? error.message : 'Failed to get response'; updateNodeInSession(sessionId, { ...node, isStreaming: false, - error: error.message || 'Failed to get response' + error: message }); // 清除流式状态 @@ -414,8 +411,9 @@ const ReactFlowWrapper: React.FC = ({ sessionId }) => { try { exportToMindmap(session); showSuccess('导出成功'); - } catch (error: any) { - showError('导出失败:' + error.message); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Unknown error'; + showError('导出失败:' + message); } }; @@ -507,6 +505,8 @@ const ReactFlowWrapper: React.FC = ({ sessionId }) => { setNodes(reactFlowNodes); setEdges(reactFlowEdges); + // Keep node callbacks bound to the current render without rebuilding this effect recursively. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [session?.nodes, sessionId, streamingResponses]); // 添加 sessionId 到依赖数组 @@ -592,4 +592,4 @@ const ChatFlow: React.FC = ({ sessionId }) => ( ); -export default ChatFlow; \ No newline at end of file +export default ChatFlow; diff --git a/src/components/FileUploadButton.tsx b/src/components/FileUploadButton.tsx index a35bb01..42a7abc 100644 --- a/src/components/FileUploadButton.tsx +++ b/src/components/FileUploadButton.tsx @@ -3,7 +3,7 @@ import { useDropzone } from 'react-dropzone'; import { FileUp, X, Check } from 'lucide-react'; import { extractTextFromFiles } from '../utils/fileUtils'; import { FileExtractResult } from '../types'; -import { showSuccess, showInfo, showWarning, showError } from '../utils/notification'; +import { showSuccess, showError } from '../utils/notification'; interface FileUploadButtonProps { onUploadComplete: (text: string) => void; @@ -19,8 +19,6 @@ const FileUploadButton: React.FC = ({ onUploadComplete, c const { getRootProps, getInputProps, isDragActive } = useDropzone({ accept: { - 'application/pdf': ['.pdf'], - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], 'text/plain': ['.txt'], 'text/markdown': ['.md'], }, @@ -48,9 +46,10 @@ const FileUploadButton: React.FC = ({ onUploadComplete, c setIsUploading(false); }, 1500); showSuccess('文件已上传'); - } catch (err: any) { - setError(err.message || 'Failed to extract text from files'); - showError('文件上传失败:' + err.message); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Failed to extract text from files'; + setError(message); + showError('文件上传失败:' + message); setIsUploading(false); } }, @@ -117,7 +116,7 @@ const FileUploadButton: React.FC = ({ onUploadComplete, c 拖拽文件到此处,或点击选择文件

- 支持 PDF, DOCX, TXT, MD 文件格式 + 支持 TXT 和 MD 文件格式

)} @@ -161,4 +160,4 @@ const FileUploadButton: React.FC = ({ onUploadComplete, c ); }; -export default FileUploadButton; \ No newline at end of file +export default FileUploadButton; diff --git a/src/components/ModelManager.tsx b/src/components/ModelManager.tsx index 603baeb..02bd794 100644 --- a/src/components/ModelManager.tsx +++ b/src/components/ModelManager.tsx @@ -3,7 +3,7 @@ import { X, Plus, Save, Trash2 } from 'lucide-react'; import { useModelStore } from '../stores/modelStore'; import { Model } from '../types'; import { gsap } from 'gsap'; -import { showSuccess, showInfo, showWarning, showError } from '../utils/notification'; + interface ModelManagerProps { onClose: () => void; } @@ -284,4 +284,4 @@ const ModelManager: React.FC = ({ onClose }) => { ); }; -export default ModelManager; \ No newline at end of file +export default ModelManager; diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 1ccc0ee..e1a8fb1 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -2,10 +2,10 @@ import React, { useState, useRef, useEffect } from 'react'; import { useSessionStore } from '../stores/sessionStore'; import { Search, Plus, Settings, Trash2, Edit, X, ChevronLeft, - MessageSquare, Library, Home, MoreHorizontal + MessageSquare, Library, MoreHorizontal } from 'lucide-react'; import { gsap } from 'gsap'; -import { showSuccess, showWarning, showError, showInfo } from '../utils/notification'; +import { showSuccess, showWarning, showInfo } from '../utils/notification'; interface SidebarProps { onModelManagerClick: () => void; @@ -101,7 +101,7 @@ const Sidebar: React.FC = ({ onModelManagerClick, collapsed, onTog
-

TreeChat

+

TreeAI

@@ -218,4 +218,4 @@ const Sidebar: React.FC = ({ onModelManagerClick, collapsed, onTog ); }; -export default Sidebar; \ No newline at end of file +export default Sidebar; diff --git a/src/components/nodes/ChatNode.tsx b/src/components/nodes/ChatNode.tsx index c381810..52c33a4 100644 --- a/src/components/nodes/ChatNode.tsx +++ b/src/components/nodes/ChatNode.tsx @@ -1,25 +1,20 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; -import { Handle, Position } from 'reactflow'; +import { Handle, Position, NodeProps } from 'reactflow'; import { MdPreview } from 'md-editor-rt'; import 'md-editor-rt/lib/preview.css'; -import { Plus, Send, RefreshCcw, Copy, Settings, Trash2, MessageSquare, MoreHorizontal } from 'lucide-react'; +import { Plus, Send, RefreshCcw, Copy, Settings, Trash2, MessageSquare } from 'lucide-react'; import { useModelStore } from '../../stores/modelStore'; import { gsap } from 'gsap'; import { showSuccess, showInfo, showWarning } from '../../utils/notification'; +import { NodeData } from '../../types'; -interface ChatNodeProps { - id: string; - data: any; -} - -const ChatNode: React.FC = ({ id, data }) => { +const ChatNode: React.FC> = ({ id, data }) => { const { node, streamingResponse, onEdit, onAddChild, onDelete, onRetry, onModelChange, onTemperatureChange, onMaxTokensChange } = data; const [userMessage, setUserMessage] = useState(node.userMessage || ''); const [isEditingUser, setIsEditingUser] = useState(!node.userMessage); const [showSettings, setShowSettings] = useState(false); const { models } = useModelStore(); - const model = models.find(m => m.id === node.modelId); const userInputRef = useRef(null); const nodeRef = useRef(null); @@ -385,4 +380,4 @@ const ChatNode: React.FC = ({ id, data }) => { ); }; -export default ChatNode; \ No newline at end of file +export default ChatNode; diff --git a/src/components/nodes/SystemNode.tsx b/src/components/nodes/SystemNode.tsx index 6a89e56..62d10a6 100644 --- a/src/components/nodes/SystemNode.tsx +++ b/src/components/nodes/SystemNode.tsx @@ -1,22 +1,17 @@ import React, { useState, useEffect, useRef } from 'react'; -import { Handle, Position } from 'reactflow'; -import { Plus, Settings, Trash2 } from 'lucide-react'; +import { Handle, Position, NodeProps } from 'reactflow'; +import { Plus, Settings } from 'lucide-react'; import { useModelStore } from '../../stores/modelStore'; import { gsap } from 'gsap'; +import { NodeData } from '../../types'; -interface SystemNodeProps { - id: string; - data: any; -} - -const SystemNode: React.FC = ({ id, data }) => { - const { node, onEdit, onAddChild, onDelete, onModelChange, onTemperatureChange, onMaxTokensChange } = data; +const SystemNode: React.FC> = ({ id, data }) => { + const { node, onEdit, onAddChild, onModelChange, onTemperatureChange, onMaxTokensChange } = data; const [isEditing, setIsEditing] = useState(false); const [showSettings, setShowSettings] = useState(false); const [systemPrompt, setSystemPrompt] = useState(node.userMessage || ''); const { models } = useModelStore(); - const model = models.find(m => m.id === node.modelId); const nodeRef = useRef(null); const textareaRef = useRef(null); @@ -193,4 +188,4 @@ const SystemNode: React.FC = ({ id, data }) => { ); }; -export default SystemNode; \ No newline at end of file +export default SystemNode; diff --git a/src/context/DatabaseContext.tsx b/src/context/DatabaseContext.tsx index 5eef471..a4163d7 100644 --- a/src/context/DatabaseContext.tsx +++ b/src/context/DatabaseContext.tsx @@ -39,10 +39,11 @@ export const DatabaseProvider: React.FC<{ children: ReactNode }> = ({ children } ); }; +// eslint-disable-next-line react-refresh/only-export-components export const useDatabaseContext = () => { const context = useContext(DatabaseContext); if (!context) { throw new Error('useDatabaseContext must be used within a DatabaseProvider'); } return context; -}; \ No newline at end of file +}; diff --git a/src/db/db.ts b/src/db/db.ts index 9fc3812..7cb7797 100644 --- a/src/db/db.ts +++ b/src/db/db.ts @@ -1,5 +1,5 @@ import Dexie, { Table } from 'dexie'; -import { Session, Model, ChatNode } from '../types'; +import { Session, Model } from '../types'; class TreeChatDatabase extends Dexie { sessions!: Table; @@ -55,4 +55,4 @@ class TreeChatDatabase extends Dexie { } const db = new TreeChatDatabase(); -export default db; \ No newline at end of file +export default db; diff --git a/src/main.tsx b/src/main.tsx index 997b9fe..6f6fe55 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -13,7 +13,7 @@ window.addEventListener('unhandledrejection', (event) => { console.error('Unhandled rejection:', event.reason); }); -console.log('TreeChat App starting...'); +console.log('TreeAI app starting...'); const rootElement = document.getElementById('root'); if (!rootElement) { @@ -28,4 +28,4 @@ if (!rootElement) { ); console.log('App rendered successfully'); -} \ No newline at end of file +} diff --git a/src/services/apiService.ts b/src/services/apiService.ts index 489b260..875446f 100644 --- a/src/services/apiService.ts +++ b/src/services/apiService.ts @@ -1,4 +1,4 @@ -import { Model, ModelResponse } from '../types'; +import { Model } from '../types'; interface ChatMessage { role: 'system' | 'user' | 'assistant'; @@ -14,13 +14,9 @@ interface ChatRequestOptions { onChunk: (chunk: string) => void; } -const activeAbortControllers = new Map(); - export async function sendChatRequest(options: ChatRequestOptions): Promise { const { messages, model, temperature, maxTokens, signal, onChunk } = options; - const requestId = crypto.randomUUID(); - try { const isOpenAI = model.baseUrl.includes('openai.com'); const endpoint = isOpenAI @@ -93,26 +89,16 @@ export async function sendChatRequest(options: ChatRequestOptions): Promise void; } -export const useModelStore = create((set, get) => ({ +const getErrorMessage = (error: unknown): string => ( + error instanceof Error ? error.message : 'Unknown error' +); + +export const useModelStore = create((set) => ({ models: [], defaultModelId: null, @@ -42,8 +46,8 @@ export const useModelStore = create((set, get) => ({ }; }); showSuccess('模型创建成功'); - } catch (error: any) { - showError('模型创建失败:' + error.message); + } catch (error: unknown) { + showError('模型创建失败:' + getErrorMessage(error)); console.error('Failed to create model:', error); } }, @@ -57,8 +61,8 @@ export const useModelStore = create((set, get) => ({ ) })); showSuccess('模型更新成功'); - } catch (error: any) { - showError('模型更新失败:' + error.message); + } catch (error: unknown) { + showError('模型更新失败:' + getErrorMessage(error)); console.error('Failed to update model:', error); } }, @@ -78,9 +82,9 @@ export const useModelStore = create((set, get) => ({ }; }); showInfo('模型已删除'); - } catch (error: any) { - showError('模型删除失败:' + error.message); + } catch (error: unknown) { + showError('模型删除失败:' + getErrorMessage(error)); console.error('Failed to delete model:', error); } } -})); \ No newline at end of file +})); diff --git a/src/types.ts b/src/types.ts index 6a496ee..0fc38b1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,6 +41,7 @@ export interface Position { export interface NodeData { node: ChatNode; + streamingResponse?: string | null; onAddChild: (parentId: string) => void; onEdit: (nodeId: string, content: string, type: 'user' | 'assistant' | 'system') => void; onDelete: (nodeId: string) => void; @@ -66,4 +67,4 @@ export interface FileExtractResult { export interface NodePosition { x: number; y: number; -} \ No newline at end of file +} diff --git a/src/utils/exportUtils.ts b/src/utils/exportUtils.ts index 5fad1ca..93202f6 100644 --- a/src/utils/exportUtils.ts +++ b/src/utils/exportUtils.ts @@ -12,7 +12,7 @@ export function exportToMindmap(session: Session): void { const mindMapTree = buildMindMapTree(session); // Convert to a format suitable for export (e.g., FreeMind, XMind, etc.) - const xmlContent = convertToFreeMindXML(mindMapTree, session.title); + const xmlContent = convertToFreeMindXML(mindMapTree); // Create a downloadable file const blob = new Blob([xmlContent], { type: 'application/xml' }); @@ -106,12 +106,12 @@ function buildMindMapTree(session: Session): MindMapNode { return rootNode; } -function convertToFreeMindXML(root: MindMapNode, title: string): string { +function convertToFreeMindXML(root: MindMapNode): string { const xmlHeader = ''; const mapStart = ''; const mapEnd = ''; - const buildNodeXML = (node: MindMapNode, isRoot: boolean = false): string => { + const buildNodeXML = (node: MindMapNode): string => { const nodeStart = ``; let content = nodeStart; @@ -126,7 +126,7 @@ function convertToFreeMindXML(root: MindMapNode, title: string): string { return content; }; - const xml = `${xmlHeader}\n${mapStart}\n${buildNodeXML(root, true)}\n${mapEnd}`; + const xml = `${xmlHeader}\n${mapStart}\n${buildNodeXML(root)}\n${mapEnd}`; return xml; } @@ -138,8 +138,3 @@ function escapeXml(text: string): string { .replace(/"/g, '"') .replace(/'/g, '''); } - -function truncateText(text: string, maxLength: number): string { - if (text.length <= maxLength) return text; - return text.substring(0, maxLength) + '...'; -} \ No newline at end of file diff --git a/src/utils/fileUtils.ts b/src/utils/fileUtils.ts index 8e26bcd..9ce26f0 100644 --- a/src/utils/fileUtils.ts +++ b/src/utils/fileUtils.ts @@ -7,15 +7,11 @@ export async function extractTextFromFiles(files: File[]): Promise { reader.onload = (e) => { resolve(e.target?.result as string || ''); }; - reader.onerror = (e) => { + reader.onerror = () => { reject(new Error('Failed to read text file')); }; reader.readAsText(file); }); } - -async function extractTextFromPdf(file: File): Promise { - throw new Error('PDF text extraction is not implemented in this browser environment.'); - - // In a real implementation, this would use a PDF.js or similar library - // Example pseudo-code: - // const pdfData = await readFileAsArrayBuffer(file); - // const pdf = await pdfjs.getDocument(pdfData).promise; - // let text = ''; - // for (let i = 1; i <= pdf.numPages; i++) { - // const page = await pdf.getPage(i); - // const content = await page.getTextContent(); - // text += content.items.map(item => item.str).join(' ') + '\n'; - // } - // return text; -} - -async function extractTextFromDocx(file: File): Promise { - throw new Error('DOCX text extraction is not implemented in this browser environment.'); - - // In a real implementation, this would use mammoth.js or similar library - // Example pseudo-code: - // const docxData = await readFileAsArrayBuffer(file); - // const result = await mammoth.extractRawText({ arrayBuffer: docxData }); - // return result.value; -} - -async function readFileAsArrayBuffer(file: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = (e) => { - resolve(e.target?.result as ArrayBuffer); - }; - reader.onerror = () => { - reject(new Error('Failed to read file')); - }; - reader.readAsArrayBuffer(file); - }); -} \ No newline at end of file diff --git a/todo.md b/todo.md index 28460a3..73bda15 100644 --- a/todo.md +++ b/todo.md @@ -1,3 +1,6 @@ -1. 输出状态下reactflow区域频繁闪烁,无法编辑文本框中内容 -2. 不支持思考模型 -3. \ No newline at end of file +# TreeAI Roadmap and Known Issues + +- [ ] Reduce React Flow canvas flicker while a response is streaming so text fields remain comfortable to edit. +- [ ] Add an explicit compatibility layer for reasoning models and provider-specific thinking streams. +- [ ] Add automated tests for branch context assembly, persistence, streaming parsing, import, and export. +- [ ] Split the production bundle to reduce the initial download size.