feat: add desktop agent workbench
This commit is contained in:
@@ -0,0 +1,128 @@
|
|||||||
|
name: Desktop CI Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- feat/embed-desktop
|
||||||
|
paths:
|
||||||
|
- ".github/workflows/desktop-ci-build.yml"
|
||||||
|
- "desktop/**"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
RUST_BACKTRACE: short
|
||||||
|
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: "${{ github.workflow }}-${{ github.ref }}"
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build ${{ matrix.name }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- name: macOS ARM64 DMG
|
||||||
|
os: macos-14
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
artifact_name: BoosAPI-Desktop-macos-arm64-dmg
|
||||||
|
artifact_path: desktop/src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/*.dmg
|
||||||
|
tauri_args: --target aarch64-apple-darwin --bundles dmg
|
||||||
|
- name: Windows x64
|
||||||
|
os: windows-latest
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
artifact_name: BoosAPI-Desktop-windows-x64
|
||||||
|
artifact_path: |
|
||||||
|
desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi
|
||||||
|
desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
|
||||||
|
desktop/src-tauri/target/x86_64-pc-windows-msvc/release/BoosApi.exe
|
||||||
|
tauri_args: --target x86_64-pc-windows-msvc
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Rust cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
prefix-key: "v1"
|
||||||
|
key: "${{ runner.os }}-${{ matrix.target }}-desktop-bun"
|
||||||
|
workspaces: desktop/src-tauri -> target
|
||||||
|
|
||||||
|
- name: Install Protoc
|
||||||
|
uses: arduino/setup-protoc@v3
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: Get linked dependency versions
|
||||||
|
id: dep-versions
|
||||||
|
shell: bash
|
||||||
|
working-directory: desktop
|
||||||
|
run: |
|
||||||
|
SM_SHA=$(git -C libs/stream-monaco rev-parse HEAD)
|
||||||
|
MS_SHA=$(git -C libs/markstream-vue rev-parse HEAD)
|
||||||
|
echo "cache-key=${{ runner.os }}-${{ matrix.target }}-desktop-linked-deps-${SM_SHA:0:12}-${MS_SHA:0:12}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Cache linked dependencies
|
||||||
|
id: cache-linked-deps
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
desktop/libs/stream-monaco/node_modules
|
||||||
|
desktop/libs/stream-monaco/dist
|
||||||
|
desktop/libs/markstream-vue/node_modules
|
||||||
|
desktop/libs/markstream-vue/packages/markdown-parser/node_modules
|
||||||
|
desktop/libs/markstream-vue/packages/markdown-parser/dist
|
||||||
|
desktop/libs/markstream-vue/packages/markstream-react/node_modules
|
||||||
|
desktop/libs/markstream-vue/packages/markstream-react/dist
|
||||||
|
key: ${{ steps.dep-versions.outputs.cache-key }}
|
||||||
|
|
||||||
|
- name: Build linked dependencies
|
||||||
|
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
||||||
|
shell: bash
|
||||||
|
working-directory: desktop
|
||||||
|
run: |
|
||||||
|
bun --cwd libs/stream-monaco install
|
||||||
|
bun --cwd libs/stream-monaco run build
|
||||||
|
|
||||||
|
bun --cwd libs/markstream-vue install
|
||||||
|
bun --cwd libs/markstream-vue run build:parser
|
||||||
|
bun --cwd libs/markstream-vue/packages/markstream-react run build
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
working-directory: desktop
|
||||||
|
run: bun install
|
||||||
|
|
||||||
|
- name: Build Desktop
|
||||||
|
working-directory: desktop
|
||||||
|
env:
|
||||||
|
APPLE_SIGNING_IDENTITY: ${{ startsWith(matrix.os, 'macos') && '-' || '' }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
|
run: bun run tauri build ${{ matrix.tauri_args }}
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact_name }}
|
||||||
|
path: ${{ matrix.artifact_path }}
|
||||||
|
if-no-files-found: error
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Desktop Agent Workbench Integration Design
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This deliverable adds the desktop UI shell and local interactions for Agent Workbench, and connects Agent mode to the existing desktop Agent command path. It intentionally avoids new Rust commands, database tables, billing, and remote media generation until the page contract is stable.
|
||||||
|
|
||||||
|
## Frontend Boundaries
|
||||||
|
|
||||||
|
- `types/index.ts` owns the `PageKey` addition.
|
||||||
|
- `components/layout/Sidebar.tsx` owns navigation entry placement.
|
||||||
|
- `components/layout/ContentArea.tsx` owns routing from `PageKey` to page component.
|
||||||
|
- `pages/AgentWorkbenchPage.tsx` owns the creator composer, local attachment state, mode state, model selection, Agent handoff, and placeholder result/status for media modes.
|
||||||
|
- `stores/providerStore.ts` remains the source of provider/model data. The page reads `providers` and calls `fetchProviders`; it does not duplicate provider fetching logic.
|
||||||
|
- `stores/conversationStore.ts` remains the source for conversation creation and Agent streaming. The Workbench reuses `createConversation`, `updateConversation`, and `sendAgentMessage`.
|
||||||
|
- `stores/skillStore.ts` remains the source for skill installation/listing. The Workbench reuses `loadSkills`, `installSkill`, and `toggleSkill` for third-party skills.
|
||||||
|
- `lib/browserMock.ts` mirrors skill install/list/toggle enough for browser preview.
|
||||||
|
- `i18n/locales/*.json` owns navigation labels. The first page UI can use Chinese literal copy to match current product direction, but nav should use locale keys.
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
Provider store -> enabled providers -> enabled chat models -> AgentWorkbenchPage model options -> selected provider/model or custom model fallback.
|
||||||
|
|
||||||
|
File input -> browser `File` objects -> base64 `AttachmentInput` payload -> local attachment chips -> existing Agent attachment persistence during `agent_query`.
|
||||||
|
|
||||||
|
Mode selector -> local `creationMode` -> mode label/model controls/result copy.
|
||||||
|
|
||||||
|
Agent submit -> create conversation -> set `mode: "agent"` -> upsert Agent session via `agent_update_session` -> `sendAgentMessage` -> Chat page streaming display.
|
||||||
|
|
||||||
|
Non-Agent submit -> local result/status card. No external side effects yet.
|
||||||
|
|
||||||
|
Skills menu -> detect `jianying-editor` -> if missing install `luoluoluo22/jianying-editor-skill` via existing `install_skill` command -> if disabled enable it -> insert `请使用 jianying-editor Skill 自动创建剪映草稿:` into the prompt -> Agent submit loads enabled skills in the backend and can invoke the skill tool.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
Existing pages and settings should be unaffected. Adding a `PageKey` requires exhaustive switch and sidebar updates in the same change.
|
||||||
|
|
||||||
|
## Follow-up Hooks
|
||||||
|
|
||||||
|
Future work can replace the non-Agent local submit handler with:
|
||||||
|
|
||||||
|
- BoosAPI `/api/agent/models` for remote model catalog.
|
||||||
|
- BoosAPI `/api/jimeng/*` for image/video generation.
|
||||||
|
- Tauri file staging command for persistent uploads.
|
||||||
|
- A dedicated JianYing setup/status panel for Python dependency checks, JianYing draft path detection, and platform/version warnings.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Implementation Plan
|
||||||
|
|
||||||
|
1. Add `agent-workbench` to desktop page types, sidebar nav, content routing, and nav locale files.
|
||||||
|
2. Create `AgentWorkbenchPage.tsx` with:
|
||||||
|
- mode definitions
|
||||||
|
- provider-derived model options
|
||||||
|
- custom model fallback
|
||||||
|
- local attachment selection/removal with `AttachmentInput` conversion
|
||||||
|
- mode dropdown, auto toggle, skills and subject popovers
|
||||||
|
- Agent mode submit that creates an Agent conversation/session and calls existing streaming flow
|
||||||
|
- skills menu action for `luoluoluo22/jianying-editor-skill` install/enable and prompt priming
|
||||||
|
- placeholder submit status/result for media modes
|
||||||
|
3. Extend browser preview mock skill commands so install/list/toggle reflects the JianYing skill integration.
|
||||||
|
4. Add focused CSS to `desktop/src/index.css`, using restrained desktop styling and avoiding global side effects.
|
||||||
|
5. Run validation:
|
||||||
|
- `bun run typecheck`
|
||||||
|
- `bun run build`
|
||||||
|
6. Review diff for unrelated churn before final report.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- `bun run typecheck` passed in `/root/metapi/desktop` after JianYing skill integration.
|
||||||
|
- `bun run build` passed in `/root/metapi/desktop` after JianYing skill integration.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Desktop Agent Workbench integration
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Add a first usable Agent Workbench entry to the desktop client so users can start from the same creator-style surface used in the BoosAPI web UI: choose a creation mode, select a model, attach references, and type a prompt.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Add an Agent Workbench page to the desktop navigation without disrupting existing Chat, Drawing, Skills, Knowledge, Memory, Gateway, Files, or Settings flows.
|
||||||
|
- The first screen should match the creator-composer direction: centered prompt, large input surface, upload affordance, creation mode selector, auto toggle, model selector, skills button, subject button, and submit button.
|
||||||
|
- Creation modes must include at least Agent, image generation, video generation, music generation, voice generation, digital human, and action imitation. Unsupported modes may show “coming soon” copy but must remain selectable.
|
||||||
|
- Agent mode should expose a conversation model selector populated from existing enabled desktop provider models where possible, with a custom model fallback.
|
||||||
|
- Upload affordance should accept local reference files, display removable attachment chips, and pass attachments into existing Agent chat flow when Agent mode submits.
|
||||||
|
- The page must follow the desktop app’s existing React/Ant Design/Zustand conventions and be reachable from the sidebar.
|
||||||
|
- The implementation must avoid new backend schema. Existing Tauri commands may be reused.
|
||||||
|
- The Workbench skills menu should expose a JianYing/剪映 automation entry that installs/enables `luoluoluo22/jianying-editor-skill` through the existing Desktop skills system and primes the Agent prompt.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] Sidebar contains an Agent Workbench entry and selecting it renders the new page.
|
||||||
|
- [x] Agent Workbench page has a centered creator composer with mode dropdown, model control, upload button, `@` subject popover, and submit button.
|
||||||
|
- [x] Available chat models are derived from existing enabled desktop providers/models, with a custom model option.
|
||||||
|
- [x] Selecting a mode updates the composer label and mode-specific model controls where applicable.
|
||||||
|
- [x] Local file selection displays attachment chips and supports removing them.
|
||||||
|
- [x] Agent mode creates an Agent conversation/session and sends the prompt plus attachments through the existing Agent stream flow.
|
||||||
|
- [x] Skills menu can install/enable the JianYing editor skill and insert a matching Agent prompt prefix.
|
||||||
|
- [x] Unsupported submit paths do not crash; they show a clear in-page result/status.
|
||||||
|
- [x] `bun run typecheck` and `bun run build` pass in `/root/metapi/desktop`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
|
||||||
|
- Lightweight tasks can remain PRD-only.
|
||||||
|
- For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start`.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "desktop-agent-workbench",
|
||||||
|
"name": "desktop-agent-workbench",
|
||||||
|
"title": "Desktop Agent Workbench integration",
|
||||||
|
"description": "",
|
||||||
|
"status": "in_progress",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"package": "vendor/ComfyUI_frontend",
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "boos4721",
|
||||||
|
"assignee": "boos4721",
|
||||||
|
"createdAt": "2026-06-25",
|
||||||
|
"completedAt": null,
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "feat/embed-desktop",
|
||||||
|
"worktree_path": null,
|
||||||
|
"commit": null,
|
||||||
|
"pr_url": null,
|
||||||
|
"subtasks": [],
|
||||||
|
"children": [],
|
||||||
|
"parent": null,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"notes": "",
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
+20
-26
@@ -134,13 +134,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
bun-version: latest
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- name: Get linked dependency versions
|
- name: Get linked dependency versions
|
||||||
id: dep-versions
|
id: dep-versions
|
||||||
@@ -168,15 +165,15 @@ jobs:
|
|||||||
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
pnpm --dir libs/stream-monaco install --frozen-lockfile
|
bun --cwd libs/stream-monaco install --frozen-lockfile
|
||||||
pnpm --dir libs/stream-monaco build
|
bun --cwd libs/stream-monaco run build
|
||||||
|
|
||||||
pnpm --dir libs/markstream-vue install --frozen-lockfile
|
bun --cwd libs/markstream-vue install --frozen-lockfile
|
||||||
pnpm --dir libs/markstream-vue run build:parser
|
bun --cwd libs/markstream-vue run build:parser
|
||||||
pnpm --dir libs/markstream-vue/packages/markstream-react build
|
bun --cwd libs/markstream-vue/packages/markstream-react run build
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build with Tauri
|
- name: Build with Tauri
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
@@ -192,7 +189,7 @@ jobs:
|
|||||||
releaseBody: ${{ needs.draft-release-notes.outputs.body }}
|
releaseBody: ${{ needs.draft-release-notes.outputs.body }}
|
||||||
releaseDraft: true
|
releaseDraft: true
|
||||||
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
prerelease: ${{ contains(github.ref_name, '-rc') }}
|
||||||
tauriScript: pnpm tauri
|
tauriScript: bun run tauri
|
||||||
args: --target ${{ matrix.target }}
|
args: --target ${{ matrix.target }}
|
||||||
includeUpdaterJson: true
|
includeUpdaterJson: true
|
||||||
|
|
||||||
@@ -268,13 +265,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
bun-version: latest
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- name: Get linked dependency versions
|
- name: Get linked dependency versions
|
||||||
id: dep-versions
|
id: dep-versions
|
||||||
@@ -300,22 +294,22 @@ jobs:
|
|||||||
- name: Build linked dependencies
|
- name: Build linked dependencies
|
||||||
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
pnpm --dir libs/stream-monaco install --frozen-lockfile
|
bun --cwd libs/stream-monaco install --frozen-lockfile
|
||||||
pnpm --dir libs/stream-monaco build
|
bun --cwd libs/stream-monaco run build
|
||||||
|
|
||||||
pnpm --dir libs/markstream-vue install --frozen-lockfile
|
bun --cwd libs/markstream-vue install --frozen-lockfile
|
||||||
pnpm --dir libs/markstream-vue run build:parser
|
bun --cwd libs/markstream-vue run build:parser
|
||||||
pnpm --dir libs/markstream-vue/packages/markstream-react build
|
bun --cwd libs/markstream-vue/packages/markstream-react run build
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build for Linux ARM64
|
- name: Build for Linux ARM64
|
||||||
run: |
|
run: |
|
||||||
export PKG_CONFIG_ALLOW_CROSS=1
|
export PKG_CONFIG_ALLOW_CROSS=1
|
||||||
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
|
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
|
||||||
export PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/
|
export PKG_CONFIG_SYSROOT_DIR=/usr/aarch64-linux-gnu/
|
||||||
pnpm tauri build --target aarch64-unknown-linux-gnu --bundles deb,rpm
|
bun run tauri build --target aarch64-unknown-linux-gnu --bundles deb,rpm
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: '--max_old_space_size=4096'
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
|
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
|
||||||
|
|||||||
+10
-13
@@ -41,13 +41,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
bun-version: latest
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- name: Get linked dependency versions
|
- name: Get linked dependency versions
|
||||||
id: dep-versions
|
id: dep-versions
|
||||||
@@ -73,15 +70,15 @@ jobs:
|
|||||||
- name: Build linked dependencies
|
- name: Build linked dependencies
|
||||||
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
pnpm --dir libs/stream-monaco install --frozen-lockfile
|
bun --cwd libs/stream-monaco install --frozen-lockfile
|
||||||
pnpm --dir libs/stream-monaco build
|
bun --cwd libs/stream-monaco run build
|
||||||
|
|
||||||
pnpm --dir libs/markstream-vue install --frozen-lockfile
|
bun --cwd libs/markstream-vue install --frozen-lockfile
|
||||||
pnpm --dir libs/markstream-vue run build:parser
|
bun --cwd libs/markstream-vue run build:parser
|
||||||
pnpm --dir libs/markstream-vue/packages/markstream-react build
|
bun --cwd libs/markstream-vue/packages/markstream-react run build
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build with Tauri
|
- name: Build with Tauri
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
@@ -97,5 +94,5 @@ jobs:
|
|||||||
releaseBody: 'Internal test build for scrollbar fix verification.'
|
releaseBody: 'Internal test build for scrollbar fix verification.'
|
||||||
releaseDraft: true
|
releaseDraft: true
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tauriScript: pnpm tauri
|
tauriScript: bun run tauri
|
||||||
args: --target aarch64-apple-darwin
|
args: --target aarch64-apple-darwin
|
||||||
|
|||||||
+10
-13
@@ -51,13 +51,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
bun-version: latest
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- name: Get linked dependency versions
|
- name: Get linked dependency versions
|
||||||
id: dep-versions
|
id: dep-versions
|
||||||
@@ -84,18 +81,18 @@ jobs:
|
|||||||
- name: Build linked dependencies
|
- name: Build linked dependencies
|
||||||
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
||||||
run: |
|
run: |
|
||||||
pnpm --dir libs/stream-monaco install --frozen-lockfile
|
bun --cwd libs/stream-monaco install --frozen-lockfile
|
||||||
pnpm --dir libs/stream-monaco build
|
bun --cwd libs/stream-monaco run build
|
||||||
|
|
||||||
pnpm --dir libs/markstream-vue install --frozen-lockfile
|
bun --cwd libs/markstream-vue install --frozen-lockfile
|
||||||
pnpm --dir libs/markstream-vue run build:parser
|
bun --cwd libs/markstream-vue run build:parser
|
||||||
pnpm --dir libs/markstream-vue/packages/markstream-react build
|
bun --cwd libs/markstream-vue/packages/markstream-react run build
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build Linux bundles
|
- name: Build Linux bundles
|
||||||
run: pnpm tauri build --target x86_64-unknown-linux-gnu --bundles appimage,rpm,deb
|
run: bun run tauri build --target x86_64-unknown-linux-gnu --bundles appimage,rpm,deb
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: '--max_old_space_size=4096'
|
NODE_OPTIONS: '--max_old_space_size=4096'
|
||||||
BOOSAPI_TEST_BUILD: '1'
|
BOOSAPI_TEST_BUILD: '1'
|
||||||
|
|||||||
+10
-13
@@ -42,13 +42,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
bun-version: latest
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v4
|
|
||||||
with:
|
|
||||||
run_install: false
|
|
||||||
|
|
||||||
- name: Get linked dependency versions
|
- name: Get linked dependency versions
|
||||||
id: dep-versions
|
id: dep-versions
|
||||||
@@ -76,15 +73,15 @@ jobs:
|
|||||||
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
if: steps.cache-linked-deps.outputs.cache-hit != 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
pnpm --dir libs/stream-monaco install --frozen-lockfile
|
bun --cwd libs/stream-monaco install --frozen-lockfile
|
||||||
pnpm --dir libs/stream-monaco build
|
bun --cwd libs/stream-monaco run build
|
||||||
|
|
||||||
pnpm --dir libs/markstream-vue install --frozen-lockfile
|
bun --cwd libs/markstream-vue install --frozen-lockfile
|
||||||
pnpm --dir libs/markstream-vue run build:parser
|
bun --cwd libs/markstream-vue run build:parser
|
||||||
pnpm --dir libs/markstream-vue/packages/markstream-react build
|
bun --cwd libs/markstream-vue/packages/markstream-react run build
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build with Tauri
|
- name: Build with Tauri
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
@@ -98,7 +95,7 @@ jobs:
|
|||||||
releaseName: 'BoosApi Test ${{ github.ref_name }}'
|
releaseName: 'BoosApi Test ${{ github.ref_name }}'
|
||||||
releaseDraft: true
|
releaseDraft: true
|
||||||
prerelease: true
|
prerelease: true
|
||||||
tauriScript: pnpm tauri
|
tauriScript: bun run tauri
|
||||||
args: --target x86_64-pc-windows-msvc
|
args: --target x86_64-pc-windows-msvc
|
||||||
|
|
||||||
- name: Package Windows portable (zip)
|
- name: Package Windows portable (zip)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { PageKey } from '@/types';
|
import type { PageKey } from '@/types';
|
||||||
|
import { AgentWorkbenchPage } from '@/pages/AgentWorkbenchPage';
|
||||||
import { ChatPage } from '@/pages/ChatPage';
|
import { ChatPage } from '@/pages/ChatPage';
|
||||||
import { DrawingPage } from '@/pages/DrawingPage';
|
import { DrawingPage } from '@/pages/DrawingPage';
|
||||||
import { KnowledgePage } from '@/pages/KnowledgePage';
|
import { KnowledgePage } from '@/pages/KnowledgePage';
|
||||||
@@ -16,6 +17,8 @@ export function ContentArea({ activePage }: ContentAreaProps) {
|
|||||||
switch (activePage) {
|
switch (activePage) {
|
||||||
case 'chat':
|
case 'chat':
|
||||||
return <ChatPage />;
|
return <ChatPage />;
|
||||||
|
case 'agent-workbench':
|
||||||
|
return <AgentWorkbenchPage />;
|
||||||
case 'drawing':
|
case 'drawing':
|
||||||
return <DrawingPage />;
|
return <DrawingPage />;
|
||||||
case 'knowledge':
|
case 'knowledge':
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Tooltip, Avatar, theme } from 'antd';
|
import { Tooltip, Avatar, theme } from 'antd';
|
||||||
import { MessageSquare, BookOpen, Brain, Router, FolderOpen, User, Sparkles, ImagePlus } from 'lucide-react';
|
import { MessageSquare, BookOpen, Brain, Router, FolderOpen, User, Sparkles, ImagePlus, WandSparkles } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useUIStore, useSettingsStore } from '@/stores';
|
import { useUIStore, useSettingsStore } from '@/stores';
|
||||||
import { useUserProfileStore } from '@/stores/userProfileStore';
|
import { useUserProfileStore } from '@/stores/userProfileStore';
|
||||||
@@ -12,6 +12,7 @@ import type { PageKey } from '@/types';
|
|||||||
|
|
||||||
const mainNavItems: { key: PageKey; icon: React.ReactNode; labelKey: string }[] = [
|
const mainNavItems: { key: PageKey; icon: React.ReactNode; labelKey: string }[] = [
|
||||||
{ key: 'chat', icon: <MessageSquare size={18} />, labelKey: 'nav.chat' },
|
{ key: 'chat', icon: <MessageSquare size={18} />, labelKey: 'nav.chat' },
|
||||||
|
{ key: 'agent-workbench', icon: <WandSparkles size={18} />, labelKey: 'nav.agentWorkbench' },
|
||||||
{ key: 'drawing', icon: <ImagePlus size={18} />, labelKey: 'nav.drawing' },
|
{ key: 'drawing', icon: <ImagePlus size={18} />, labelKey: 'nav.drawing' },
|
||||||
{ key: 'skills', icon: <Sparkles size={18} />, labelKey: 'nav.skills' },
|
{ key: 'skills', icon: <Sparkles size={18} />, labelKey: 'nav.skills' },
|
||||||
{ key: 'knowledge', icon: <BookOpen size={18} />, labelKey: 'nav.knowledge' },
|
{ key: 'knowledge', icon: <BookOpen size={18} />, labelKey: 'nav.knowledge' },
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "المحادثة",
|
"chat": "المحادثة",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "قاعدة المعرفة",
|
"knowledge": "قاعدة المعرفة",
|
||||||
"memory": "الذاكرة",
|
"memory": "الذاكرة",
|
||||||
"gateway": "البوابة",
|
"gateway": "البوابة",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "Wissen",
|
"knowledge": "Wissen",
|
||||||
"memory": "Gedächtnis",
|
"memory": "Gedächtnis",
|
||||||
"gateway": "Gateway",
|
"gateway": "Gateway",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "Knowledge",
|
"knowledge": "Knowledge",
|
||||||
"memory": "Memory",
|
"memory": "Memory",
|
||||||
"gateway": "Gateway",
|
"gateway": "Gateway",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "Base de conocimientos",
|
"knowledge": "Base de conocimientos",
|
||||||
"memory": "Memoria",
|
"memory": "Memoria",
|
||||||
"gateway": "Pasarela",
|
"gateway": "Pasarela",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "Connaissances",
|
"knowledge": "Connaissances",
|
||||||
"memory": "Mémoire",
|
"memory": "Mémoire",
|
||||||
"gateway": "Passerelle",
|
"gateway": "Passerelle",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "चैट",
|
"chat": "चैट",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "ज्ञान आधार",
|
"knowledge": "ज्ञान आधार",
|
||||||
"memory": "स्मृति",
|
"memory": "स्मृति",
|
||||||
"gateway": "गेटवे",
|
"gateway": "गेटवे",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "チャット",
|
"chat": "チャット",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "ナレッジ",
|
"knowledge": "ナレッジ",
|
||||||
"memory": "メモリ",
|
"memory": "メモリ",
|
||||||
"gateway": "ゲートウェイ",
|
"gateway": "ゲートウェイ",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "채팅",
|
"chat": "채팅",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "지식",
|
"knowledge": "지식",
|
||||||
"memory": "메모리",
|
"memory": "메모리",
|
||||||
"gateway": "게이트웨이",
|
"gateway": "게이트웨이",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "Чат",
|
"chat": "Чат",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "База знаний",
|
"knowledge": "База знаний",
|
||||||
"memory": "Память",
|
"memory": "Память",
|
||||||
"gateway": "Шлюз",
|
"gateway": "Шлюз",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "对话",
|
"chat": "对话",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "知识库",
|
"knowledge": "知识库",
|
||||||
"memory": "记忆",
|
"memory": "记忆",
|
||||||
"gateway": "网关",
|
"gateway": "网关",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "對話",
|
"chat": "對話",
|
||||||
|
"agentWorkbench": "Agent",
|
||||||
"knowledge": "知識庫",
|
"knowledge": "知識庫",
|
||||||
"memory": "記憶",
|
"memory": "記憶",
|
||||||
"gateway": "閘道",
|
"gateway": "閘道",
|
||||||
|
|||||||
@@ -29,6 +29,365 @@ body {
|
|||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.agent-workbench-desktop {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-desktop-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(247, 249, 251, 0.96), rgba(248, 250, 252, 0.98)),
|
||||||
|
radial-gradient(circle at 18% 18%, rgba(22, 119, 255, 0.08), transparent 28%),
|
||||||
|
radial-gradient(circle at 82% 78%, rgba(20, 184, 166, 0.08), transparent 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="dark"] .agent-workbench-desktop-bg {
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(12, 18, 24, 0.98), rgba(14, 18, 22, 0.98)),
|
||||||
|
radial-gradient(circle at 18% 18%, rgba(22, 119, 255, 0.12), transparent 28%),
|
||||||
|
radial-gradient(circle at 82% 78%, rgba(20, 184, 166, 0.1), transparent 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-desktop-hero {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
align-content: center;
|
||||||
|
gap: 54px;
|
||||||
|
height: 100%;
|
||||||
|
padding: 48px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-desktop-hero h1 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: clamp(28px, 4vw, 42px);
|
||||||
|
font-weight: 780;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-desktop-composer {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 96px minmax(0, 1fr);
|
||||||
|
grid-template-rows: minmax(118px, auto) auto auto;
|
||||||
|
gap: 10px 18px;
|
||||||
|
width: min(78vw, 1280px);
|
||||||
|
min-height: 232px;
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border-color) 86%, transparent);
|
||||||
|
border-radius: 26px;
|
||||||
|
background: color-mix(in srgb, var(--color-bg-elevated) 94%, transparent);
|
||||||
|
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-upload {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 72px;
|
||||||
|
height: 92px;
|
||||||
|
margin-top: 4px;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border-color) 92%, transparent);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: color-mix(in srgb, var(--color-fill-alter) 80%, var(--color-bg-elevated));
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transform: rotate(-8deg);
|
||||||
|
transition: transform 0.16s ease, border-color 0.16s ease, color 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-upload:hover {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
color: var(--color-primary);
|
||||||
|
transform: rotate(-5deg) translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-prompt {
|
||||||
|
align-self: stretch;
|
||||||
|
border: 0 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
color: var(--color-text) !important;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.7;
|
||||||
|
resize: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-prompt textarea,
|
||||||
|
.agent-workbench-prompt.ant-input {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-attachments {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-attachments .ant-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 260px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-attachments .ant-tag span {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-tool-left {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-pill {
|
||||||
|
min-height: 44px;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
font-weight: 720;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-pill.active {
|
||||||
|
color: var(--color-primary);
|
||||||
|
background: color-mix(in srgb, var(--color-primary) 8%, var(--color-bg-elevated));
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-pill .ant-btn-icon,
|
||||||
|
.agent-workbench-pill > span:not(.ant-btn-icon) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-pill .ant-switch {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-pill.square {
|
||||||
|
width: 44px;
|
||||||
|
padding-inline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-model-select {
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-model-select .ant-select-selector,
|
||||||
|
.agent-workbench-model-input {
|
||||||
|
min-height: 44px;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-model-input {
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-submit {
|
||||||
|
width: 92px;
|
||||||
|
min-height: 44px;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-result {
|
||||||
|
width: min(78vw, 920px);
|
||||||
|
padding: 18px 20px;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--color-primary) 20%, var(--border-color));
|
||||||
|
border-radius: 18px;
|
||||||
|
background: color-mix(in srgb, var(--color-primary) 5%, var(--color-bg-elevated));
|
||||||
|
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-result strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-result p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-mode-menu .ant-dropdown-menu {
|
||||||
|
width: 300px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-mode-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 24px minmax(0, 1fr) 18px;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-mode-item > span {
|
||||||
|
display: inline-flex;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-mode-item div {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-mode-item strong {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-mode-item small {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-mode-item em {
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-popover,
|
||||||
|
.agent-workbench-subject-popover {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: var(--color-bg-elevated);
|
||||||
|
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-popover {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
width: 420px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-popover button {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 24px minmax(0, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-popover button:hover {
|
||||||
|
background: var(--color-fill-alter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-popover small {
|
||||||
|
display: block;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-subject-popover {
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
|
width: 310px;
|
||||||
|
min-height: 300px;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-subject-popover > span {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-subject-popover > div {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
align-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
min-height: 220px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-subject-popover button {
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--color-fill-alter);
|
||||||
|
color: var(--color-text);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 760;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.agent-workbench-desktop-composer,
|
||||||
|
.agent-workbench-result {
|
||||||
|
width: min(92vw, 980px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-desktop-composer {
|
||||||
|
grid-template-columns: 74px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-upload {
|
||||||
|
width: 58px;
|
||||||
|
height: 78px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.agent-workbench-desktop-hero {
|
||||||
|
justify-content: stretch;
|
||||||
|
padding: 28px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-desktop-composer {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-upload {
|
||||||
|
width: 54px;
|
||||||
|
height: 68px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-toolbar {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-workbench-submit {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Remove antd focus outline (green border) globally */
|
/* Remove antd focus outline (green border) globally */
|
||||||
*:focus-visible {
|
*:focus-visible {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
|
|||||||
@@ -1584,7 +1584,7 @@ export async function handleCommand<T>(cmd: string, args?: Record<string, unknow
|
|||||||
|
|
||||||
// ── Skills ────────────────────────────────────────────────────────
|
// ── Skills ────────────────────────────────────────────────────────
|
||||||
case 'list_skills':
|
case 'list_skills':
|
||||||
return [] as T;
|
return getStore<any[]>('skills', []) as T;
|
||||||
|
|
||||||
case 'get_skill':
|
case 'get_skill':
|
||||||
return {
|
return {
|
||||||
@@ -1602,11 +1602,36 @@ export async function handleCommand<T>(cmd: string, args?: Record<string, unknow
|
|||||||
manifest: null,
|
manifest: null,
|
||||||
} as T;
|
} as T;
|
||||||
|
|
||||||
case 'toggle_skill':
|
case 'toggle_skill': {
|
||||||
|
const { name, enabled } = args as any;
|
||||||
|
const skills = getStore<any[]>('skills', []);
|
||||||
|
setStore('skills', skills.map((skill: any) => (
|
||||||
|
skill.name === name ? { ...skill, enabled } : skill
|
||||||
|
)));
|
||||||
return undefined as T;
|
return undefined as T;
|
||||||
|
}
|
||||||
|
|
||||||
case 'install_skill':
|
case 'install_skill': {
|
||||||
return ((args as any)?.source || 'installed-skill') as T;
|
const source = (args as any)?.source || 'installed-skill';
|
||||||
|
const name = source.includes('jianying-editor-skill') ? 'jianying-editor' : String(source).split('/').pop();
|
||||||
|
const skills = getStore<any[]>('skills', []);
|
||||||
|
const nextSkill = {
|
||||||
|
name,
|
||||||
|
description: name === 'jianying-editor'
|
||||||
|
? '剪映 AI 自动化剪辑 Skill,支持草稿生成、素材导入、字幕、配音、特效和导出。'
|
||||||
|
: 'Installed skill',
|
||||||
|
source: 'boosapi',
|
||||||
|
sourcePath: `/mock/skills/${name}`,
|
||||||
|
enabled: true,
|
||||||
|
hasUpdate: false,
|
||||||
|
userInvocable: true,
|
||||||
|
};
|
||||||
|
setStore('skills', [
|
||||||
|
nextSkill,
|
||||||
|
...skills.filter((skill: any) => skill.name !== name),
|
||||||
|
]);
|
||||||
|
return source as T;
|
||||||
|
}
|
||||||
|
|
||||||
case 'uninstall_skill':
|
case 'uninstall_skill':
|
||||||
return undefined as T;
|
return undefined as T;
|
||||||
|
|||||||
@@ -0,0 +1,526 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { App, Button, Dropdown, Input, Select, Switch, Tag, theme } from 'antd';
|
||||||
|
import type { MenuProps } from 'antd';
|
||||||
|
import {
|
||||||
|
ArrowUp,
|
||||||
|
AtSign,
|
||||||
|
ChevronDown,
|
||||||
|
Clapperboard,
|
||||||
|
ImagePlus,
|
||||||
|
Mic2,
|
||||||
|
Music2,
|
||||||
|
Plus,
|
||||||
|
SlidersHorizontal,
|
||||||
|
Sparkles,
|
||||||
|
UserRound,
|
||||||
|
WandSparkles,
|
||||||
|
Wrench,
|
||||||
|
X,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { invoke } from '@/lib/invoke';
|
||||||
|
import { useConversationStore, useProviderStore, useSkillStore, useUIStore } from '@/stores';
|
||||||
|
import type { AttachmentInput, Model } from '@/types';
|
||||||
|
|
||||||
|
type CreationMode = 'agent' | 'image' | 'video' | 'music' | 'voice' | 'digital-human' | 'action';
|
||||||
|
|
||||||
|
interface CreationModeOption {
|
||||||
|
key: CreationMode;
|
||||||
|
label: string;
|
||||||
|
hint: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AttachmentItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
size: number;
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatModelOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
providerId: string;
|
||||||
|
modelId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CREATION_MODES: CreationModeOption[] = [
|
||||||
|
{ key: 'agent', label: 'Agent 模式', hint: '拆解任务、读写文件、执行工具', icon: <WandSparkles size={16} /> },
|
||||||
|
{ key: 'image', label: '图片生成', hint: '即梦图片、参考图与风格控制', icon: <ImagePlus size={16} /> },
|
||||||
|
{ key: 'video', label: '视频生成', hint: '即梦视频、图生视频与分镜', icon: <Clapperboard size={16} /> },
|
||||||
|
{ key: 'music', label: '音乐生成', hint: '配乐、氛围与短片 BGM', icon: <Music2 size={16} /> },
|
||||||
|
{ key: 'voice', label: '配音生成', hint: 'TTS 音色、语速与试听', icon: <Mic2 size={16} /> },
|
||||||
|
{ key: 'digital-human', label: '数字人', hint: '口播、形象与视频工作流', icon: <UserRound size={16} /> },
|
||||||
|
{ key: 'action', label: '动作模仿', hint: '动作迁移与主体参考', icon: <Sparkles size={16} /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
const JIMENG_IMAGE_MODELS = [
|
||||||
|
{ value: 'jimeng_t2i_v40', label: '即梦图片 4.0' },
|
||||||
|
{ value: 'custom', label: '自定义 req_key' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const JIMENG_VIDEO_MODELS = [
|
||||||
|
{ value: 'jimeng_ti2v_v30_pro', label: '即梦图生视频 3.0 Pro' },
|
||||||
|
{ value: 'custom', label: '自定义 req_key' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const JIANYING_SKILL_NAME = 'jianying-editor';
|
||||||
|
const JIANYING_SKILL_SOURCE = 'luoluoluo22/jianying-editor-skill';
|
||||||
|
const JIANYING_PROMPT_PREFIX = '请使用 jianying-editor Skill 自动创建剪映草稿:';
|
||||||
|
|
||||||
|
function formatFileSize(size: number): string {
|
||||||
|
if (size < 1024) return `${size} B`;
|
||||||
|
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
|
||||||
|
return `${(size / 1024 / 1024).toFixed(1)} MB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function modelOptionLabel(model: Model, providerName: string): string {
|
||||||
|
return `${model.name || model.model_id} · ${providerName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUsableChatModel(model: Model): boolean {
|
||||||
|
return model.enabled && (model.model_type === 'Chat' || model.capabilities.includes('TextChat'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAttachmentMimeType(fileName: string, mimeType?: string): string {
|
||||||
|
if (mimeType) return mimeType;
|
||||||
|
const extension = fileName.split('.').pop()?.toLowerCase();
|
||||||
|
if (extension === 'pdf') return 'application/pdf';
|
||||||
|
if (extension === 'txt') return 'text/plain';
|
||||||
|
if (extension === 'md') return 'text/markdown';
|
||||||
|
if (extension === 'json') return 'application/json';
|
||||||
|
return 'application/octet-stream';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fileToAttachmentItem(file: File): Promise<AttachmentItem> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onerror = () => reject(reader.error ?? new Error(`读取文件失败:${file.name}`));
|
||||||
|
reader.onload = () => {
|
||||||
|
const base64 = (reader.result as string).split(',')[1] || '';
|
||||||
|
resolve({
|
||||||
|
id: `${file.name}-${file.size}-${file.lastModified}-${globalThis.crypto?.randomUUID?.() ?? Date.now()}`,
|
||||||
|
name: file.name,
|
||||||
|
type: getAttachmentMimeType(file.name, file.type),
|
||||||
|
size: file.size,
|
||||||
|
data: base64,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toAttachmentInput(item: AttachmentItem): AttachmentInput {
|
||||||
|
return {
|
||||||
|
file_name: item.name,
|
||||||
|
file_type: item.type,
|
||||||
|
file_size: item.size,
|
||||||
|
data: item.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgentWorkbenchPage() {
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const providers = useProviderStore((s) => s.providers);
|
||||||
|
const fetchProviders = useProviderStore((s) => s.fetchProviders);
|
||||||
|
const createConversation = useConversationStore((s) => s.createConversation);
|
||||||
|
const updateConversation = useConversationStore((s) => s.updateConversation);
|
||||||
|
const sendAgentMessage = useConversationStore((s) => s.sendAgentMessage);
|
||||||
|
const skills = useSkillStore((s) => s.skills);
|
||||||
|
const loadSkills = useSkillStore((s) => s.loadSkills);
|
||||||
|
const installSkill = useSkillStore((s) => s.installSkill);
|
||||||
|
const toggleSkill = useSkillStore((s) => s.toggleSkill);
|
||||||
|
const setActivePage = useUIStore((s) => s.setActivePage);
|
||||||
|
const [creationMode, setCreationMode] = useState<CreationMode>('agent');
|
||||||
|
const [autoMode, setAutoMode] = useState(true);
|
||||||
|
const [prompt, setPrompt] = useState('');
|
||||||
|
const [agentModel, setAgentModel] = useState('custom');
|
||||||
|
const [customAgentModel, setCustomAgentModel] = useState('');
|
||||||
|
const [imageModel, setImageModel] = useState('jimeng_t2i_v40');
|
||||||
|
const [videoModel, setVideoModel] = useState('jimeng_ti2v_v30_pro');
|
||||||
|
const [customJimengModel, setCustomJimengModel] = useState('');
|
||||||
|
const [attachments, setAttachments] = useState<AttachmentItem[]>([]);
|
||||||
|
const [subjectOpen, setSubjectOpen] = useState(false);
|
||||||
|
const [skillOpen, setSkillOpen] = useState(false);
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [installingJianying, setInstallingJianying] = useState(false);
|
||||||
|
const [result, setResult] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (providers.length === 0) {
|
||||||
|
void fetchProviders().catch((error) => message.error(String(error)));
|
||||||
|
}
|
||||||
|
}, [fetchProviders, message, providers.length]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadSkills();
|
||||||
|
}, [loadSkills]);
|
||||||
|
|
||||||
|
const chatModelOptions = useMemo<ChatModelOption[]>(() => {
|
||||||
|
const rows = providers
|
||||||
|
.filter((provider) => provider.enabled)
|
||||||
|
.flatMap((provider) =>
|
||||||
|
provider.models
|
||||||
|
.filter(isUsableChatModel)
|
||||||
|
.map((model) => ({
|
||||||
|
value: `${provider.id}::${model.model_id}`,
|
||||||
|
label: modelOptionLabel(model, provider.name),
|
||||||
|
providerId: provider.id,
|
||||||
|
modelId: model.model_id,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return rows.filter((item) => {
|
||||||
|
if (seen.has(item.value)) return false;
|
||||||
|
seen.add(item.value);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [providers]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (agentModel !== 'custom') return;
|
||||||
|
if (customAgentModel || chatModelOptions.length === 0) return;
|
||||||
|
setAgentModel(chatModelOptions[0].value);
|
||||||
|
}, [agentModel, chatModelOptions, customAgentModel]);
|
||||||
|
|
||||||
|
const currentMode = CREATION_MODES.find((item) => item.key === creationMode) ?? CREATION_MODES[0];
|
||||||
|
const selectedModel = creationMode === 'agent'
|
||||||
|
? (agentModel === 'custom' ? customAgentModel.trim() || '自定义模型' : agentModel)
|
||||||
|
: creationMode === 'image'
|
||||||
|
? (imageModel === 'custom' ? customJimengModel.trim() || '自定义 req_key' : imageModel)
|
||||||
|
: creationMode === 'video'
|
||||||
|
? (videoModel === 'custom' ? customJimengModel.trim() || '自定义 req_key' : videoModel)
|
||||||
|
: '待配置';
|
||||||
|
|
||||||
|
const modeItems: MenuProps['items'] = CREATION_MODES.map((item) => ({
|
||||||
|
key: item.key,
|
||||||
|
label: (
|
||||||
|
<div className="agent-workbench-mode-item">
|
||||||
|
<span>{item.icon}</span>
|
||||||
|
<div>
|
||||||
|
<strong>{item.label}</strong>
|
||||||
|
<small>{item.hint}</small>
|
||||||
|
</div>
|
||||||
|
{item.key === creationMode && <em>✓</em>}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const selectedChatModel = useMemo(() => (
|
||||||
|
chatModelOptions.find((item) => item.value === agentModel) ?? null
|
||||||
|
), [agentModel, chatModelOptions]);
|
||||||
|
|
||||||
|
const jianyingSkill = useMemo(() => (
|
||||||
|
skills.find((item) => item.name === JIANYING_SKILL_NAME) ?? null
|
||||||
|
), [skills]);
|
||||||
|
|
||||||
|
const handleFilesSelected = useCallback((files: FileList | null) => {
|
||||||
|
if (!files || files.length === 0) return;
|
||||||
|
void Promise.all(Array.from(files).map(fileToAttachmentItem))
|
||||||
|
.then((next) => {
|
||||||
|
setAttachments((current) => [...current, ...next]);
|
||||||
|
})
|
||||||
|
.catch((error) => message.error(String(error)))
|
||||||
|
.finally(() => {
|
||||||
|
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||||
|
});
|
||||||
|
}, [message]);
|
||||||
|
|
||||||
|
const removeAttachment = useCallback((id: string) => {
|
||||||
|
setAttachments((current) => current.filter((item) => item.id !== id));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const applyJianyingSkill = useCallback(async () => {
|
||||||
|
if (installingJianying) return;
|
||||||
|
setInstallingJianying(true);
|
||||||
|
try {
|
||||||
|
if (!jianyingSkill) {
|
||||||
|
await installSkill(JIANYING_SKILL_SOURCE);
|
||||||
|
message.success('剪映 Skill 已安装');
|
||||||
|
} else if (!jianyingSkill.enabled) {
|
||||||
|
await toggleSkill(JIANYING_SKILL_NAME, true);
|
||||||
|
message.success('剪映 Skill 已启用');
|
||||||
|
}
|
||||||
|
setCreationMode('agent');
|
||||||
|
setPrompt((value) => {
|
||||||
|
const text = value.trim();
|
||||||
|
if (text.includes(JIANYING_SKILL_NAME) || text.includes('剪映')) return value;
|
||||||
|
return text ? `${JIANYING_PROMPT_PREFIX}${text}` : `${JIANYING_PROMPT_PREFIX}`;
|
||||||
|
});
|
||||||
|
setSkillOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
message.error(`剪映 Skill 集成失败:${String(error)}`);
|
||||||
|
} finally {
|
||||||
|
setInstallingJianying(false);
|
||||||
|
}
|
||||||
|
}, [installSkill, installingJianying, jianyingSkill, message, toggleSkill]);
|
||||||
|
|
||||||
|
const submit = useCallback(async () => {
|
||||||
|
const text = prompt.trim();
|
||||||
|
if (!text) {
|
||||||
|
message.warning('先输入想法、剧本或任务说明');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (submitting) return;
|
||||||
|
|
||||||
|
if (creationMode !== 'agent') {
|
||||||
|
setResult(`${currentMode.label} 已准备好。模型:${selectedModel};参考素材:${attachments.length} 个。即梦/媒体生成链路下一步接入。`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackProvider = providers.find((provider) => provider.enabled);
|
||||||
|
const providerId = selectedChatModel?.providerId ?? fallbackProvider?.id;
|
||||||
|
const modelId = selectedChatModel?.modelId ?? customAgentModel.trim();
|
||||||
|
if (!providerId || !modelId) {
|
||||||
|
message.warning('先在供应商里启用一个对话模型,或填写自定义模型名');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
const title = text.slice(0, 24) || 'Agent 创作';
|
||||||
|
const conversation = await createConversation(title, modelId, providerId);
|
||||||
|
await updateConversation(conversation.id, { mode: 'agent', provider_id: providerId, model_id: modelId });
|
||||||
|
await invoke('agent_update_session', {
|
||||||
|
conversationId: conversation.id,
|
||||||
|
permissionMode: autoMode ? 'accept_edits' : 'default',
|
||||||
|
});
|
||||||
|
await sendAgentMessage(text, attachments.map(toAttachmentInput));
|
||||||
|
setResult(`Agent 已开始执行。模型:${modelId};参考素材:${attachments.length} 个。`);
|
||||||
|
setPrompt('');
|
||||||
|
setAttachments([]);
|
||||||
|
setActivePage('chat');
|
||||||
|
} catch (error) {
|
||||||
|
message.error(String(error));
|
||||||
|
setResult(`Agent 启动失败:${String(error)}`);
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
attachments,
|
||||||
|
autoMode,
|
||||||
|
createConversation,
|
||||||
|
creationMode,
|
||||||
|
currentMode.label,
|
||||||
|
customAgentModel,
|
||||||
|
message,
|
||||||
|
prompt,
|
||||||
|
providers,
|
||||||
|
selectedChatModel,
|
||||||
|
selectedModel,
|
||||||
|
sendAgentMessage,
|
||||||
|
setActivePage,
|
||||||
|
submitting,
|
||||||
|
updateConversation,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="agent-workbench-desktop" style={{ background: token.colorBgContainer }}>
|
||||||
|
<div className="agent-workbench-desktop-bg" />
|
||||||
|
<section className="agent-workbench-desktop-hero">
|
||||||
|
<h1>你好,想创作什么?</h1>
|
||||||
|
<div className="agent-workbench-desktop-composer">
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
className="agent-workbench-file-input"
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept="image/*,video/*,audio/*,.pdf,.txt,.md,.json"
|
||||||
|
onChange={(event) => handleFilesSelected(event.target.files)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="agent-workbench-upload"
|
||||||
|
type="button"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
aria-label="上传参考素材"
|
||||||
|
>
|
||||||
|
<Plus size={24} />
|
||||||
|
</button>
|
||||||
|
<Input.TextArea
|
||||||
|
className="agent-workbench-prompt"
|
||||||
|
value={prompt}
|
||||||
|
onChange={(event) => setPrompt(event.target.value)}
|
||||||
|
onPressEnter={(event) => {
|
||||||
|
if (event.metaKey || event.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
void submit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="输入想法、剧本或上传参考,支持 “/” 使用技能,@ 添加主体,和 Agent 一起创作"
|
||||||
|
autoSize={{ minRows: 4, maxRows: 8 }}
|
||||||
|
/>
|
||||||
|
{attachments.length > 0 && (
|
||||||
|
<div className="agent-workbench-attachments">
|
||||||
|
{attachments.map((item) => (
|
||||||
|
<Tag
|
||||||
|
key={item.id}
|
||||||
|
closeIcon={<X size={12} />}
|
||||||
|
onClose={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
removeAttachment(item.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.name} <span>{formatFileSize(item.size)}</span>
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="agent-workbench-toolbar">
|
||||||
|
<div className="agent-workbench-tool-left">
|
||||||
|
<Dropdown
|
||||||
|
trigger={['click']}
|
||||||
|
menu={{
|
||||||
|
items: modeItems,
|
||||||
|
selectable: true,
|
||||||
|
selectedKeys: [creationMode],
|
||||||
|
onClick: ({ key }) => setCreationMode(key as CreationMode),
|
||||||
|
}}
|
||||||
|
overlayClassName="agent-workbench-mode-menu"
|
||||||
|
>
|
||||||
|
<Button className="agent-workbench-pill active" icon={currentMode.icon}>
|
||||||
|
{currentMode.label} <ChevronDown size={14} />
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
<Button className="agent-workbench-pill" icon={<SlidersHorizontal size={15} />} onClick={() => setAutoMode((value) => !value)}>
|
||||||
|
自动
|
||||||
|
<Switch size="small" checked={autoMode} onChange={setAutoMode} onClick={(_, event) => event.stopPropagation()} />
|
||||||
|
</Button>
|
||||||
|
{creationMode === 'agent' && (
|
||||||
|
<>
|
||||||
|
<Select
|
||||||
|
className="agent-workbench-model-select"
|
||||||
|
value={agentModel}
|
||||||
|
onChange={(value) => {
|
||||||
|
setAgentModel(value);
|
||||||
|
if (value !== 'custom') setCustomAgentModel('');
|
||||||
|
}}
|
||||||
|
options={[
|
||||||
|
...chatModelOptions,
|
||||||
|
{ value: 'custom', label: '自定义模型' },
|
||||||
|
]}
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
/>
|
||||||
|
{agentModel === 'custom' && (
|
||||||
|
<Input
|
||||||
|
className="agent-workbench-model-input"
|
||||||
|
value={customAgentModel}
|
||||||
|
onChange={(event) => setCustomAgentModel(event.target.value)}
|
||||||
|
placeholder="输入 BoosAPI 模型名"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{creationMode === 'image' && (
|
||||||
|
<>
|
||||||
|
<Select
|
||||||
|
className="agent-workbench-model-select"
|
||||||
|
value={imageModel}
|
||||||
|
onChange={setImageModel}
|
||||||
|
options={JIMENG_IMAGE_MODELS}
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
/>
|
||||||
|
{imageModel === 'custom' && (
|
||||||
|
<Input
|
||||||
|
className="agent-workbench-model-input"
|
||||||
|
value={customJimengModel}
|
||||||
|
onChange={(event) => setCustomJimengModel(event.target.value)}
|
||||||
|
placeholder="输入火山 req_key"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{creationMode === 'video' && (
|
||||||
|
<>
|
||||||
|
<Select
|
||||||
|
className="agent-workbench-model-select"
|
||||||
|
value={videoModel}
|
||||||
|
onChange={setVideoModel}
|
||||||
|
options={JIMENG_VIDEO_MODELS}
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
/>
|
||||||
|
{videoModel === 'custom' && (
|
||||||
|
<Input
|
||||||
|
className="agent-workbench-model-input"
|
||||||
|
value={customJimengModel}
|
||||||
|
onChange={(event) => setCustomJimengModel(event.target.value)}
|
||||||
|
placeholder="输入火山 req_key"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Dropdown
|
||||||
|
open={skillOpen}
|
||||||
|
onOpenChange={setSkillOpen}
|
||||||
|
trigger={['click']}
|
||||||
|
dropdownRender={() => (
|
||||||
|
<div className="agent-workbench-popover">
|
||||||
|
<Input placeholder="搜索技能" prefix={<Wrench size={15} />} />
|
||||||
|
<button type="button" onClick={() => void applyJianyingSkill()}>
|
||||||
|
<Clapperboard size={16} />
|
||||||
|
剪映自动剪辑
|
||||||
|
<small>
|
||||||
|
{installingJianying
|
||||||
|
? '正在安装/启用...'
|
||||||
|
: jianyingSkill?.enabled
|
||||||
|
? '已启用,生成草稿、字幕、配音、特效和导出'
|
||||||
|
: jianyingSkill
|
||||||
|
? '已安装,点击启用并写入提示'
|
||||||
|
: '一键安装 luoluoluo22/jianying-editor-skill'}
|
||||||
|
</small>
|
||||||
|
</button>
|
||||||
|
<button type="button"><Wrench size={16} /> 剧情短片 <small>自动生成故事大纲、分镜脚本并产出短片</small></button>
|
||||||
|
<button type="button"><Wrench size={16} /> 电商套图 <small>生成风格统一的商品全套视觉素材</small></button>
|
||||||
|
<button type="button"><Plus size={16} /> 用 Agent 创建技能</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Button className="agent-workbench-pill" icon={<Wrench size={15} />}>使用技能</Button>
|
||||||
|
</Dropdown>
|
||||||
|
<Dropdown
|
||||||
|
open={subjectOpen}
|
||||||
|
onOpenChange={setSubjectOpen}
|
||||||
|
trigger={['click']}
|
||||||
|
dropdownRender={() => (
|
||||||
|
<div className="agent-workbench-subject-popover">
|
||||||
|
<span>可能@的内容</span>
|
||||||
|
<div>
|
||||||
|
<AtSign size={36} />
|
||||||
|
<strong>你还没有创建过主体</strong>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setPrompt((value) => `${value}${value && !value.endsWith(' ') ? ' ' : ''}@主体 `);
|
||||||
|
setSubjectOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+ 创建主体
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Button className="agent-workbench-pill square" icon={<AtSign size={16} />} />
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="agent-workbench-submit"
|
||||||
|
type="primary"
|
||||||
|
icon={<ArrowUp size={18} />}
|
||||||
|
loading={submitting}
|
||||||
|
onClick={() => void submit()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{result && (
|
||||||
|
<div className="agent-workbench-result">
|
||||||
|
<strong>{currentMode.label}</strong>
|
||||||
|
<p>{result}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -656,7 +656,16 @@ export interface RealtimeConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// === UI State ===
|
// === UI State ===
|
||||||
export type PageKey = 'chat' | 'drawing' | 'knowledge' | 'memory' | 'gateway' | 'files' | 'settings' | 'skills';
|
export type PageKey =
|
||||||
|
| 'chat'
|
||||||
|
| 'agent-workbench'
|
||||||
|
| 'drawing'
|
||||||
|
| 'knowledge'
|
||||||
|
| 'memory'
|
||||||
|
| 'gateway'
|
||||||
|
| 'files'
|
||||||
|
| 'settings'
|
||||||
|
| 'skills';
|
||||||
|
|
||||||
// === Drawing ===
|
// === Drawing ===
|
||||||
export type DrawingModelId = 'gpt-image-2' | 'gpt-image-1.5' | 'gpt-image-1' | 'gpt-image-1-mini';
|
export type DrawingModelId = 'gpt-image-2' | 'gpt-image-1.5' | 'gpt-image-1' | 'gpt-image-1-mini';
|
||||||
|
|||||||
Reference in New Issue
Block a user