mirror of
https://github.com/langgenius/dify.git
synced 2026-01-08 23:34:11 +00:00
Compare commits
1 Commits
feat/suppo
...
feat/crede
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8647e9582 |
94
.github/workflows/translate-i18n-base-on-english.yml
vendored
Normal file
94
.github/workflows/translate-i18n-base-on-english.yml
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
name: Translate i18n Files Based on English
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'web/i18n/en-US/*.json'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
check-and-update:
|
||||
if: github.repository == 'langgenius/dify'
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
steps:
|
||||
# Keep use old checkout action version for https://github.com/peter-evans/create-pull-request/issues/4272
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check for file changes in i18n/en-US
|
||||
id: check_files
|
||||
run: |
|
||||
# Skip check for manual trigger, translate all files
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "FILES_CHANGED=true" >> $GITHUB_ENV
|
||||
echo "FILE_ARGS=" >> $GITHUB_ENV
|
||||
echo "Manual trigger: translating all files"
|
||||
else
|
||||
git fetch origin "${{ github.event.before }}" || true
|
||||
git fetch origin "${{ github.sha }}" || true
|
||||
changed_files=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- 'i18n/en-US/*.json')
|
||||
echo "Changed files: $changed_files"
|
||||
if [ -n "$changed_files" ]; then
|
||||
echo "FILES_CHANGED=true" >> $GITHUB_ENV
|
||||
file_args=""
|
||||
for file in $changed_files; do
|
||||
filename=$(basename "$file" .json)
|
||||
file_args="$file_args --file $filename"
|
||||
done
|
||||
echo "FILE_ARGS=$file_args" >> $GITHUB_ENV
|
||||
echo "File arguments: $file_args"
|
||||
else
|
||||
echo "FILES_CHANGED=false" >> $GITHUB_ENV
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
package_json_file: web/package.json
|
||||
run_install: false
|
||||
|
||||
- name: Set up Node.js
|
||||
if: env.FILES_CHANGED == 'true'
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: pnpm
|
||||
cache-dependency-path: ./web/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
if: env.FILES_CHANGED == 'true'
|
||||
working-directory: ./web
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Generate i18n translations
|
||||
if: env.FILES_CHANGED == 'true'
|
||||
working-directory: ./web
|
||||
run: pnpm run i18n:gen ${{ env.FILE_ARGS }}
|
||||
|
||||
- name: Create Pull Request
|
||||
if: env.FILES_CHANGED == 'true'
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'chore(i18n): update translations based on en-US changes'
|
||||
title: 'chore(i18n): translate i18n files based on en-US changes'
|
||||
body: |
|
||||
This PR was automatically created to update i18n translation files based on changes in en-US locale.
|
||||
|
||||
**Triggered by:** ${{ github.sha }}
|
||||
|
||||
**Changes included:**
|
||||
- Updated translation files for all locales
|
||||
branch: chore/automated-i18n-updates-${{ github.sha }}
|
||||
delete-branch: true
|
||||
410
.github/workflows/translate-i18n-claude.yml
vendored
410
.github/workflows/translate-i18n-claude.yml
vendored
@@ -1,410 +0,0 @@
|
||||
name: Translate i18n Files with Claude Code
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'web/i18n/en-US/*.json'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
files:
|
||||
description: 'Specific files to translate (space-separated, e.g., "app common"). Leave empty for all files.'
|
||||
required: false
|
||||
type: string
|
||||
languages:
|
||||
description: 'Specific languages to translate (space-separated, e.g., "zh-Hans ja-JP"). Leave empty for all supported languages.'
|
||||
required: false
|
||||
type: string
|
||||
mode:
|
||||
description: 'Sync mode: incremental (only changes) or full (re-check all keys)'
|
||||
required: false
|
||||
default: 'incremental'
|
||||
type: choice
|
||||
options:
|
||||
- incremental
|
||||
- full
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
translate:
|
||||
if: github.repository == 'langgenius/dify'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
package_json_file: web/package.json
|
||||
run_install: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: pnpm
|
||||
cache-dependency-path: ./web/pnpm-lock.yaml
|
||||
|
||||
- name: Detect changed files and generate diff
|
||||
id: detect_changes
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
# Manual trigger
|
||||
if [ -n "${{ github.event.inputs.files }}" ]; then
|
||||
echo "CHANGED_FILES=${{ github.event.inputs.files }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
# Get all JSON files in en-US directory
|
||||
files=$(ls web/i18n/en-US/*.json 2>/dev/null | xargs -n1 basename | sed 's/.json$//' | tr '\n' ' ')
|
||||
echo "CHANGED_FILES=$files" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
echo "TARGET_LANGS=${{ github.event.inputs.languages }}" >> $GITHUB_OUTPUT
|
||||
echo "SYNC_MODE=${{ github.event.inputs.mode || 'incremental' }}" >> $GITHUB_OUTPUT
|
||||
|
||||
# For manual trigger with incremental mode, get diff from last commit
|
||||
# For full mode, we'll do a complete check anyway
|
||||
if [ "${{ github.event.inputs.mode }}" == "full" ]; then
|
||||
echo "Full mode: will check all keys" > /tmp/i18n-diff.txt
|
||||
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
git diff HEAD~1..HEAD -- 'web/i18n/en-US/*.json' > /tmp/i18n-diff.txt 2>/dev/null || echo "" > /tmp/i18n-diff.txt
|
||||
if [ -s /tmp/i18n-diff.txt ]; then
|
||||
echo "DIFF_AVAILABLE=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Push trigger - detect changed files from the push
|
||||
BEFORE_SHA="${{ github.event.before }}"
|
||||
# Handle edge case: first push or force push may have null/zero SHA
|
||||
if [ -z "$BEFORE_SHA" ] || [ "$BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
||||
# Fallback to comparing with parent commit
|
||||
BEFORE_SHA="HEAD~1"
|
||||
fi
|
||||
changed=$(git diff --name-only "$BEFORE_SHA" ${{ github.sha }} -- 'web/i18n/en-US/*.json' 2>/dev/null | xargs -n1 basename 2>/dev/null | sed 's/.json$//' | tr '\n' ' ' || echo "")
|
||||
echo "CHANGED_FILES=$changed" >> $GITHUB_OUTPUT
|
||||
echo "TARGET_LANGS=" >> $GITHUB_OUTPUT
|
||||
echo "SYNC_MODE=incremental" >> $GITHUB_OUTPUT
|
||||
|
||||
# Generate detailed diff for the push
|
||||
git diff "$BEFORE_SHA"..${{ github.sha }} -- 'web/i18n/en-US/*.json' > /tmp/i18n-diff.txt 2>/dev/null || echo "" > /tmp/i18n-diff.txt
|
||||
if [ -s /tmp/i18n-diff.txt ]; then
|
||||
echo "DIFF_AVAILABLE=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "DIFF_AVAILABLE=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
|
||||
# Truncate diff if too large (keep first 50KB)
|
||||
if [ -f /tmp/i18n-diff.txt ]; then
|
||||
head -c 50000 /tmp/i18n-diff.txt > /tmp/i18n-diff-truncated.txt
|
||||
mv /tmp/i18n-diff-truncated.txt /tmp/i18n-diff.txt
|
||||
fi
|
||||
|
||||
echo "Detected files: $(cat $GITHUB_OUTPUT | grep CHANGED_FILES || echo 'none')"
|
||||
|
||||
- name: Run Claude Code for Translation Sync
|
||||
if: steps.detect_changes.outputs.CHANGED_FILES != ''
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prompt: |
|
||||
You are a professional i18n synchronization engineer for the Dify project.
|
||||
Your task is to keep all language translations in sync with the English source (en-US).
|
||||
|
||||
## CRITICAL TOOL RESTRICTIONS
|
||||
- Use **Read** tool to read files (NOT cat or bash)
|
||||
- Use **Edit** tool to modify JSON files (NOT node, jq, or bash scripts)
|
||||
- Use **Bash** ONLY for: git commands, gh commands, pnpm commands
|
||||
- Run bash commands ONE BY ONE, never combine with && or ||
|
||||
- NEVER use `$()` command substitution - it's not supported. Split into separate commands instead.
|
||||
|
||||
## WORKING DIRECTORY & ABSOLUTE PATHS
|
||||
Claude Code sandbox working directory may vary. Always use absolute paths:
|
||||
- For pnpm: `pnpm --dir ${{ github.workspace }}/web <command>`
|
||||
- For git: `git -C ${{ github.workspace }} <command>`
|
||||
- For gh: `gh --repo ${{ github.repository }} <command>`
|
||||
- For file paths: `${{ github.workspace }}/web/i18n/`
|
||||
|
||||
## EFFICIENCY RULES
|
||||
- **ONE Edit per language file** - batch all key additions into a single Edit
|
||||
- Insert new keys at the beginning of JSON (after `{`), lint:fix will sort them
|
||||
- Translate ALL keys for a language mentally first, then do ONE Edit
|
||||
|
||||
## Context
|
||||
- Changed/target files: ${{ steps.detect_changes.outputs.CHANGED_FILES }}
|
||||
- Target languages (empty means all supported): ${{ steps.detect_changes.outputs.TARGET_LANGS }}
|
||||
- Sync mode: ${{ steps.detect_changes.outputs.SYNC_MODE }}
|
||||
- Translation files are located in: ${{ github.workspace }}/web/i18n/{locale}/{filename}.json
|
||||
- Language configuration is in: ${{ github.workspace }}/web/i18n-config/languages.ts
|
||||
- Git diff is available: ${{ steps.detect_changes.outputs.DIFF_AVAILABLE }}
|
||||
|
||||
## CRITICAL DESIGN: Verify First, Then Sync
|
||||
|
||||
You MUST follow this three-phase approach:
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
║ PHASE 1: VERIFY - Analyze and Generate Change Report ║
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
### Step 1.1: Analyze Git Diff (for incremental mode)
|
||||
Use the Read tool to read `/tmp/i18n-diff.txt` to see the git diff.
|
||||
|
||||
Parse the diff to categorize changes:
|
||||
- Lines with `+` (not `+++`): Added or modified values
|
||||
- Lines with `-` (not `---`): Removed or old values
|
||||
- Identify specific keys for each category:
|
||||
* ADD: Keys that appear only in `+` lines (new keys)
|
||||
* UPDATE: Keys that appear in both `-` and `+` lines (value changed)
|
||||
* DELETE: Keys that appear only in `-` lines (removed keys)
|
||||
|
||||
### Step 1.2: Read Language Configuration
|
||||
Use the Read tool to read `${{ github.workspace }}/web/i18n-config/languages.ts`.
|
||||
Extract all languages with `supported: true`.
|
||||
|
||||
### Step 1.3: Run i18n:check for Each Language
|
||||
```bash
|
||||
pnpm --dir ${{ github.workspace }}/web install --frozen-lockfile
|
||||
```
|
||||
```bash
|
||||
pnpm --dir ${{ github.workspace }}/web run i18n:check
|
||||
```
|
||||
|
||||
This will report:
|
||||
- Missing keys (need to ADD)
|
||||
- Extra keys (need to DELETE)
|
||||
|
||||
### Step 1.4: Generate Change Report
|
||||
|
||||
Create a structured report identifying:
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ I18N SYNC CHANGE REPORT ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Files to process: [list] ║
|
||||
║ Languages to sync: [list] ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ ADD (New Keys): ║
|
||||
║ - [filename].[key]: "English value" ║
|
||||
║ ... ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ UPDATE (Modified Keys - MUST re-translate): ║
|
||||
║ - [filename].[key]: "Old value" → "New value" ║
|
||||
║ ... ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ DELETE (Extra Keys): ║
|
||||
║ - [language]/[filename].[key] ║
|
||||
║ ... ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
**IMPORTANT**: For UPDATE detection, compare git diff to find keys where
|
||||
the English value changed. These MUST be re-translated even if target
|
||||
language already has a translation (it's now stale!).
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
║ PHASE 2: SYNC - Execute Changes Based on Report ║
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
### Step 2.1: Process ADD Operations (BATCH per language file)
|
||||
|
||||
**CRITICAL WORKFLOW for efficiency:**
|
||||
1. First, translate ALL new keys for ALL languages mentally
|
||||
2. Then, for EACH language file, do ONE Edit operation:
|
||||
- Read the file once
|
||||
- Insert ALL new keys at the beginning (right after the opening `{`)
|
||||
- Don't worry about alphabetical order - lint:fix will sort them later
|
||||
|
||||
Example Edit (adding 3 keys to zh-Hans/app.json):
|
||||
```
|
||||
old_string: '{\n "accessControl"'
|
||||
new_string: '{\n "newKey1": "translation1",\n "newKey2": "translation2",\n "newKey3": "translation3",\n "accessControl"'
|
||||
```
|
||||
|
||||
**IMPORTANT**:
|
||||
- ONE Edit per language file (not one Edit per key!)
|
||||
- Always use the Edit tool. NEVER use bash scripts, node, or jq.
|
||||
|
||||
### Step 2.2: Process UPDATE Operations
|
||||
|
||||
**IMPORTANT: Special handling for zh-Hans and ja-JP**
|
||||
If zh-Hans or ja-JP files were ALSO modified in the same push:
|
||||
- Run: `git -C ${{ github.workspace }} diff HEAD~1 --name-only` and check for zh-Hans or ja-JP files
|
||||
- If found, it means someone manually translated them. Apply these rules:
|
||||
|
||||
1. **Missing keys**: Still ADD them (completeness required)
|
||||
2. **Existing translations**: Compare with the NEW English value:
|
||||
- If translation is **completely wrong** or **unrelated** → Update it
|
||||
- If translation is **roughly correct** (captures the meaning) → Keep it, respect manual work
|
||||
- When in doubt, **keep the manual translation**
|
||||
|
||||
Example:
|
||||
- English changed: "Save" → "Save Changes"
|
||||
- Manual translation: "保存更改" → Keep it (correct meaning)
|
||||
- Manual translation: "删除" → Update it (completely wrong)
|
||||
|
||||
For other languages:
|
||||
Use Edit tool to replace the old value with the new translation.
|
||||
You can batch multiple updates in one Edit if they are adjacent.
|
||||
|
||||
### Step 2.3: Process DELETE Operations
|
||||
For extra keys reported by i18n:check:
|
||||
- Run: `pnpm --dir ${{ github.workspace }}/web run i18n:check --auto-remove`
|
||||
- Or manually remove from target language JSON files
|
||||
|
||||
## Translation Guidelines
|
||||
|
||||
- PRESERVE all placeholders exactly as-is:
|
||||
- `{{variable}}` - Mustache interpolation
|
||||
- `${variable}` - Template literal
|
||||
- `<tag>content</tag>` - HTML tags
|
||||
- `_one`, `_other` - Pluralization suffixes (these are KEY suffixes, not values)
|
||||
- Use appropriate language register (formal/informal) based on existing translations
|
||||
- Match existing translation style in each language
|
||||
- Technical terms: check existing conventions per language
|
||||
- For CJK languages: no spaces between characters unless necessary
|
||||
- For RTL languages (ar-TN, fa-IR): ensure proper text handling
|
||||
|
||||
## Output Format Requirements
|
||||
- Alphabetical key ordering (if original file uses it)
|
||||
- 2-space indentation
|
||||
- Trailing newline at end of file
|
||||
- Valid JSON (use proper escaping for special characters)
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
║ PHASE 3: RE-VERIFY - Confirm All Issues Resolved ║
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
### Step 3.1: Run Lint Fix (IMPORTANT!)
|
||||
```bash
|
||||
pnpm --dir ${{ github.workspace }}/web lint:fix --quiet -- 'i18n/**/*.json'
|
||||
```
|
||||
This ensures:
|
||||
- JSON keys are sorted alphabetically (jsonc/sort-keys rule)
|
||||
- Valid i18n keys (dify-i18n/valid-i18n-keys rule)
|
||||
- No extra keys (dify-i18n/no-extra-keys rule)
|
||||
|
||||
### Step 3.2: Run Final i18n Check
|
||||
```bash
|
||||
pnpm --dir ${{ github.workspace }}/web run i18n:check
|
||||
```
|
||||
|
||||
### Step 3.3: Fix Any Remaining Issues
|
||||
If check reports issues:
|
||||
- Go back to PHASE 2 for unresolved items
|
||||
- Repeat until check passes
|
||||
|
||||
### Step 3.4: Generate Final Summary
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ SYNC COMPLETED SUMMARY ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ Language │ Added │ Updated │ Deleted │ Status ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ zh-Hans │ 5 │ 2 │ 1 │ ✓ Complete ║
|
||||
║ ja-JP │ 5 │ 2 │ 1 │ ✓ Complete ║
|
||||
║ ... │ ... │ ... │ ... │ ... ║
|
||||
╠══════════════════════════════════════════════════════════════╣
|
||||
║ i18n:check │ PASSED - All keys in sync ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
## Mode-Specific Behavior
|
||||
|
||||
**SYNC_MODE = "incremental"** (default):
|
||||
- Focus on keys identified from git diff
|
||||
- Also check i18n:check output for any missing/extra keys
|
||||
- Efficient for small changes
|
||||
|
||||
**SYNC_MODE = "full"**:
|
||||
- Compare ALL keys between en-US and each language
|
||||
- Run i18n:check to identify all discrepancies
|
||||
- Use for first-time sync or fixing historical issues
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. Always run i18n:check BEFORE and AFTER making changes
|
||||
2. The check script is the source of truth for missing/extra keys
|
||||
3. For UPDATE scenario: git diff is the source of truth for changed values
|
||||
4. Create a single commit with all translation changes
|
||||
5. If any translation fails, continue with others and report failures
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
║ PHASE 4: COMMIT AND CREATE PR ║
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
After all translations are complete and verified:
|
||||
|
||||
### Step 4.1: Check for changes
|
||||
```bash
|
||||
git -C ${{ github.workspace }} status --porcelain
|
||||
```
|
||||
|
||||
If there are changes:
|
||||
|
||||
### Step 4.2: Create a new branch and commit
|
||||
Run these git commands ONE BY ONE (not combined with &&).
|
||||
**IMPORTANT**: Do NOT use `$()` command substitution. Use two separate commands:
|
||||
|
||||
1. First, get the timestamp:
|
||||
```bash
|
||||
date +%Y%m%d-%H%M%S
|
||||
```
|
||||
(Note the output, e.g., "20260115-143052")
|
||||
|
||||
2. Then create branch using the timestamp value:
|
||||
```bash
|
||||
git -C ${{ github.workspace }} checkout -b chore/i18n-sync-20260115-143052
|
||||
```
|
||||
(Replace "20260115-143052" with the actual timestamp from step 1)
|
||||
|
||||
3. Stage changes:
|
||||
```bash
|
||||
git -C ${{ github.workspace }} add web/i18n/
|
||||
```
|
||||
|
||||
4. Commit:
|
||||
```bash
|
||||
git -C ${{ github.workspace }} commit -m "chore(i18n): sync translations with en-US - Mode: ${{ steps.detect_changes.outputs.SYNC_MODE }}"
|
||||
```
|
||||
|
||||
5. Push:
|
||||
```bash
|
||||
git -C ${{ github.workspace }} push origin HEAD
|
||||
```
|
||||
|
||||
### Step 4.3: Create Pull Request
|
||||
```bash
|
||||
gh pr create --repo ${{ github.repository }} --title "chore(i18n): sync translations with en-US" --body "## Summary
|
||||
|
||||
This PR was automatically generated to sync i18n translation files.
|
||||
|
||||
### Changes
|
||||
- Mode: ${{ steps.detect_changes.outputs.SYNC_MODE }}
|
||||
- Files processed: ${{ steps.detect_changes.outputs.CHANGED_FILES }}
|
||||
|
||||
### Verification
|
||||
- [x] \`i18n:check\` passed
|
||||
- [x] \`lint:fix\` applied
|
||||
|
||||
🤖 Generated with Claude Code GitHub Action" --base main
|
||||
```
|
||||
|
||||
claude_args: |
|
||||
--max-turns 150
|
||||
--allowedTools "Read,Write,Edit,Bash(git *),Bash(git:*),Bash(gh *),Bash(gh:*),Bash(pnpm *),Bash(pnpm:*),Bash(date *),Bash(date:*),Glob,Grep"
|
||||
@@ -1,14 +1,14 @@
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from flask import make_response, redirect, request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, model_validator
|
||||
from flask_restx import Resource, reqparse
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import BadRequest, Forbidden
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.common.schema import register_schema_models
|
||||
from controllers.web.error import NotFoundError
|
||||
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||
from core.plugin.entities.plugin_daemon import CredentialType
|
||||
@@ -35,38 +35,35 @@ from ..wraps import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TriggerSubscriptionBuilderCreatePayload(BaseModel):
|
||||
credential_type: str = CredentialType.UNAUTHORIZED
|
||||
class TriggerSubscriptionUpdateRequest(BaseModel):
|
||||
"""Request payload for updating a trigger subscription"""
|
||||
|
||||
|
||||
class TriggerSubscriptionBuilderVerifyPayload(BaseModel):
|
||||
credentials: dict[str, Any]
|
||||
|
||||
|
||||
class TriggerSubscriptionBuilderUpdatePayload(BaseModel):
|
||||
name: str | None = None
|
||||
parameters: dict[str, Any] | None = None
|
||||
properties: dict[str, Any] | None = None
|
||||
credentials: dict[str, Any] | None = None
|
||||
name: str | None = Field(default=None, description="The name for the subscription")
|
||||
credentials: Mapping[str, Any] | None = Field(default=None, description="The credentials for the subscription")
|
||||
parameters: Mapping[str, Any] | None = Field(default=None, description="The parameters for the subscription")
|
||||
properties: Mapping[str, Any] | None = Field(default=None, description="The properties for the subscription")
|
||||
|
||||
@model_validator(mode="after")
|
||||
def check_at_least_one_field(self):
|
||||
if all(v is None for v in self.model_dump().values()):
|
||||
if all(v is None for v in (self.name, self.credentials, self.parameters, self.properties)):
|
||||
raise ValueError("At least one of name, credentials, parameters, or properties must be provided")
|
||||
return self
|
||||
|
||||
|
||||
class TriggerOAuthClientPayload(BaseModel):
|
||||
client_params: dict[str, Any] | None = None
|
||||
enabled: bool | None = None
|
||||
class TriggerSubscriptionVerifyRequest(BaseModel):
|
||||
"""Request payload for verifying subscription credentials."""
|
||||
|
||||
credentials: Mapping[str, Any] = Field(description="The credentials to verify")
|
||||
|
||||
|
||||
register_schema_models(
|
||||
console_ns,
|
||||
TriggerSubscriptionBuilderCreatePayload,
|
||||
TriggerSubscriptionBuilderVerifyPayload,
|
||||
TriggerSubscriptionBuilderUpdatePayload,
|
||||
TriggerOAuthClientPayload,
|
||||
console_ns.schema_model(
|
||||
TriggerSubscriptionUpdateRequest.__name__,
|
||||
TriggerSubscriptionUpdateRequest.model_json_schema(ref_template="#/definitions/{model}"),
|
||||
)
|
||||
|
||||
console_ns.schema_model(
|
||||
TriggerSubscriptionVerifyRequest.__name__,
|
||||
TriggerSubscriptionVerifyRequest.model_json_schema(ref_template="#/definitions/{model}"),
|
||||
)
|
||||
|
||||
|
||||
@@ -135,11 +132,16 @@ class TriggerSubscriptionListApi(Resource):
|
||||
raise
|
||||
|
||||
|
||||
parser = reqparse.RequestParser().add_argument(
|
||||
"credential_type", type=str, required=False, nullable=True, location="json"
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route(
|
||||
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/create",
|
||||
)
|
||||
class TriggerSubscriptionBuilderCreateApi(Resource):
|
||||
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderCreatePayload.__name__])
|
||||
@console_ns.expect(parser)
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@@ -149,10 +151,10 @@ class TriggerSubscriptionBuilderCreateApi(Resource):
|
||||
user = current_user
|
||||
assert user.current_tenant_id is not None
|
||||
|
||||
payload = TriggerSubscriptionBuilderCreatePayload.model_validate(console_ns.payload or {})
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
credential_type = CredentialType.of(payload.credential_type)
|
||||
credential_type = CredentialType.of(args.get("credential_type") or CredentialType.UNAUTHORIZED.value)
|
||||
subscription_builder = TriggerSubscriptionBuilderService.create_trigger_subscription_builder(
|
||||
tenant_id=user.current_tenant_id,
|
||||
user_id=user.id,
|
||||
@@ -180,11 +182,18 @@ class TriggerSubscriptionBuilderGetApi(Resource):
|
||||
)
|
||||
|
||||
|
||||
parser_api = (
|
||||
reqparse.RequestParser()
|
||||
# The credentials of the subscription builder
|
||||
.add_argument("credentials", type=dict, required=False, nullable=True, location="json")
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route(
|
||||
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/verify-and-update/<path:subscription_builder_id>",
|
||||
)
|
||||
class TriggerSubscriptionBuilderVerifyApi(Resource):
|
||||
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderVerifyPayload.__name__])
|
||||
class TriggerSubscriptionBuilderVerifyAndUpdateApi(Resource):
|
||||
@console_ns.expect(parser_api)
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@@ -194,7 +203,7 @@ class TriggerSubscriptionBuilderVerifyApi(Resource):
|
||||
user = current_user
|
||||
assert user.current_tenant_id is not None
|
||||
|
||||
payload = TriggerSubscriptionBuilderVerifyPayload.model_validate(console_ns.payload or {})
|
||||
args = parser_api.parse_args()
|
||||
|
||||
try:
|
||||
# Use atomic update_and_verify to prevent race conditions
|
||||
@@ -204,7 +213,7 @@ class TriggerSubscriptionBuilderVerifyApi(Resource):
|
||||
provider_id=TriggerProviderID(provider),
|
||||
subscription_builder_id=subscription_builder_id,
|
||||
subscription_builder_updater=SubscriptionBuilderUpdater(
|
||||
credentials=payload.credentials,
|
||||
credentials=args.get("credentials", None),
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -212,11 +221,24 @@ class TriggerSubscriptionBuilderVerifyApi(Resource):
|
||||
raise ValueError(str(e)) from e
|
||||
|
||||
|
||||
parser_update_api = (
|
||||
reqparse.RequestParser()
|
||||
# The name of the subscription builder
|
||||
.add_argument("name", type=str, required=False, nullable=True, location="json")
|
||||
# The parameters of the subscription builder
|
||||
.add_argument("parameters", type=dict, required=False, nullable=True, location="json")
|
||||
# The properties of the subscription builder
|
||||
.add_argument("properties", type=dict, required=False, nullable=True, location="json")
|
||||
# The credentials of the subscription builder
|
||||
.add_argument("credentials", type=dict, required=False, nullable=True, location="json")
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route(
|
||||
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/update/<path:subscription_builder_id>",
|
||||
)
|
||||
class TriggerSubscriptionBuilderUpdateApi(Resource):
|
||||
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderUpdatePayload.__name__])
|
||||
@console_ns.expect(parser_update_api)
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@@ -227,7 +249,7 @@ class TriggerSubscriptionBuilderUpdateApi(Resource):
|
||||
assert isinstance(user, Account)
|
||||
assert user.current_tenant_id is not None
|
||||
|
||||
payload = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {})
|
||||
args = parser_update_api.parse_args()
|
||||
try:
|
||||
return jsonable_encoder(
|
||||
TriggerSubscriptionBuilderService.update_trigger_subscription_builder(
|
||||
@@ -235,10 +257,10 @@ class TriggerSubscriptionBuilderUpdateApi(Resource):
|
||||
provider_id=TriggerProviderID(provider),
|
||||
subscription_builder_id=subscription_builder_id,
|
||||
subscription_builder_updater=SubscriptionBuilderUpdater(
|
||||
name=payload.name,
|
||||
parameters=payload.parameters,
|
||||
properties=payload.properties,
|
||||
credentials=payload.credentials,
|
||||
name=args.get("name", None),
|
||||
parameters=args.get("parameters", None),
|
||||
properties=args.get("properties", None),
|
||||
credentials=args.get("credentials", None),
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -273,7 +295,7 @@ class TriggerSubscriptionBuilderLogsApi(Resource):
|
||||
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/build/<path:subscription_builder_id>",
|
||||
)
|
||||
class TriggerSubscriptionBuilderBuildApi(Resource):
|
||||
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderUpdatePayload.__name__])
|
||||
@console_ns.expect(parser_update_api)
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@@ -282,7 +304,7 @@ class TriggerSubscriptionBuilderBuildApi(Resource):
|
||||
"""Build a subscription instance for a trigger provider"""
|
||||
user = current_user
|
||||
assert user.current_tenant_id is not None
|
||||
payload = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {})
|
||||
args = parser_update_api.parse_args()
|
||||
try:
|
||||
# Use atomic update_and_build to prevent race conditions
|
||||
TriggerSubscriptionBuilderService.update_and_build_builder(
|
||||
@@ -291,9 +313,9 @@ class TriggerSubscriptionBuilderBuildApi(Resource):
|
||||
provider_id=TriggerProviderID(provider),
|
||||
subscription_builder_id=subscription_builder_id,
|
||||
subscription_builder_updater=SubscriptionBuilderUpdater(
|
||||
name=payload.name,
|
||||
parameters=payload.parameters,
|
||||
properties=payload.properties,
|
||||
name=args.get("name", None),
|
||||
parameters=args.get("parameters", None),
|
||||
properties=args.get("properties", None),
|
||||
),
|
||||
)
|
||||
return 200
|
||||
@@ -306,7 +328,7 @@ class TriggerSubscriptionBuilderBuildApi(Resource):
|
||||
"/workspaces/current/trigger-provider/<path:subscription_id>/subscriptions/update",
|
||||
)
|
||||
class TriggerSubscriptionUpdateApi(Resource):
|
||||
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderUpdatePayload.__name__])
|
||||
@console_ns.expect(console_ns.models[TriggerSubscriptionUpdateRequest.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@@ -316,7 +338,7 @@ class TriggerSubscriptionUpdateApi(Resource):
|
||||
user = current_user
|
||||
assert user.current_tenant_id is not None
|
||||
|
||||
request = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {})
|
||||
request = TriggerSubscriptionUpdateRequest.model_validate(console_ns.payload)
|
||||
|
||||
subscription = TriggerProviderService.get_subscription_by_id(
|
||||
tenant_id=user.current_tenant_id,
|
||||
@@ -546,6 +568,13 @@ class TriggerOAuthCallbackApi(Resource):
|
||||
return redirect(f"{dify_config.CONSOLE_WEB_URL}/oauth-callback")
|
||||
|
||||
|
||||
parser_oauth_client = (
|
||||
reqparse.RequestParser()
|
||||
.add_argument("client_params", type=dict, required=False, nullable=True, location="json")
|
||||
.add_argument("enabled", type=bool, required=False, nullable=True, location="json")
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/trigger-provider/<path:provider>/oauth/client")
|
||||
class TriggerOAuthClientManageApi(Resource):
|
||||
@setup_required
|
||||
@@ -593,7 +622,7 @@ class TriggerOAuthClientManageApi(Resource):
|
||||
logger.exception("Error getting OAuth client", exc_info=e)
|
||||
raise
|
||||
|
||||
@console_ns.expect(console_ns.models[TriggerOAuthClientPayload.__name__])
|
||||
@console_ns.expect(parser_oauth_client)
|
||||
@setup_required
|
||||
@login_required
|
||||
@is_admin_or_owner_required
|
||||
@@ -603,15 +632,15 @@ class TriggerOAuthClientManageApi(Resource):
|
||||
user = current_user
|
||||
assert user.current_tenant_id is not None
|
||||
|
||||
payload = TriggerOAuthClientPayload.model_validate(console_ns.payload or {})
|
||||
args = parser_oauth_client.parse_args()
|
||||
|
||||
try:
|
||||
provider_id = TriggerProviderID(provider)
|
||||
return TriggerProviderService.save_custom_oauth_client_params(
|
||||
tenant_id=user.current_tenant_id,
|
||||
provider_id=provider_id,
|
||||
client_params=payload.client_params,
|
||||
enabled=payload.enabled,
|
||||
client_params=args.get("client_params"),
|
||||
enabled=args.get("enabled"),
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
@@ -647,7 +676,7 @@ class TriggerOAuthClientManageApi(Resource):
|
||||
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/verify/<path:subscription_id>",
|
||||
)
|
||||
class TriggerSubscriptionVerifyApi(Resource):
|
||||
@console_ns.expect(console_ns.models[TriggerSubscriptionBuilderVerifyPayload.__name__])
|
||||
@console_ns.expect(console_ns.models[TriggerSubscriptionVerifyRequest.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@@ -657,7 +686,9 @@ class TriggerSubscriptionVerifyApi(Resource):
|
||||
user = current_user
|
||||
assert user.current_tenant_id is not None
|
||||
|
||||
verify_request = TriggerSubscriptionBuilderVerifyPayload.model_validate(console_ns.payload or {})
|
||||
verify_request: TriggerSubscriptionVerifyRequest = TriggerSubscriptionVerifyRequest.model_validate(
|
||||
console_ns.payload
|
||||
)
|
||||
|
||||
try:
|
||||
result = TriggerProviderService.verify_subscription_credentials(
|
||||
|
||||
@@ -10,12 +10,7 @@ from controllers.console.auth.error import (
|
||||
InvalidEmailError,
|
||||
)
|
||||
from controllers.console.error import AccountBannedError
|
||||
from controllers.console.wraps import (
|
||||
decrypt_code_field,
|
||||
decrypt_password_field,
|
||||
only_edition_enterprise,
|
||||
setup_required,
|
||||
)
|
||||
from controllers.console.wraps import only_edition_enterprise, setup_required
|
||||
from controllers.web import web_ns
|
||||
from controllers.web.wraps import decode_jwt_token
|
||||
from libs.helper import email
|
||||
@@ -47,7 +42,6 @@ class LoginApi(Resource):
|
||||
404: "Account not found",
|
||||
}
|
||||
)
|
||||
@decrypt_password_field
|
||||
def post(self):
|
||||
"""Authenticate user and login."""
|
||||
parser = (
|
||||
@@ -187,7 +181,6 @@ class EmailCodeLoginApi(Resource):
|
||||
404: "Account not found",
|
||||
}
|
||||
)
|
||||
@decrypt_code_field
|
||||
def post(self):
|
||||
parser = (
|
||||
reqparse.RequestParser()
|
||||
|
||||
@@ -39,6 +39,7 @@ from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
from extensions.otel import WorkflowAppRunnerHandler, trace_span
|
||||
from models import Workflow
|
||||
from models.enums import UserFrom
|
||||
from models.model import App, Conversation, Message, MessageAnnotation
|
||||
from models.workflow import ConversationVariable
|
||||
from services.conversation_variable_updater import ConversationVariableUpdater
|
||||
@@ -105,11 +106,6 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
|
||||
if not app_record:
|
||||
raise ValueError("App not found")
|
||||
|
||||
invoke_from = self.application_generate_entity.invoke_from
|
||||
if self.application_generate_entity.single_iteration_run or self.application_generate_entity.single_loop_run:
|
||||
invoke_from = InvokeFrom.DEBUGGER
|
||||
user_from = self._resolve_user_from(invoke_from)
|
||||
|
||||
if self.application_generate_entity.single_iteration_run or self.application_generate_entity.single_loop_run:
|
||||
# Handle single iteration or single loop run
|
||||
graph, variable_pool, graph_runtime_state = self._prepare_single_node_execution(
|
||||
@@ -162,8 +158,6 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
|
||||
workflow_id=self._workflow.id,
|
||||
tenant_id=self._workflow.tenant_id,
|
||||
user_id=self.application_generate_entity.user_id,
|
||||
user_from=user_from,
|
||||
invoke_from=invoke_from,
|
||||
)
|
||||
|
||||
db.session.close()
|
||||
@@ -181,8 +175,12 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
|
||||
graph=graph,
|
||||
graph_config=self._workflow.graph_dict,
|
||||
user_id=self.application_generate_entity.user_id,
|
||||
user_from=user_from,
|
||||
invoke_from=invoke_from,
|
||||
user_from=(
|
||||
UserFrom.ACCOUNT
|
||||
if self.application_generate_entity.invoke_from in {InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER}
|
||||
else UserFrom.END_USER
|
||||
),
|
||||
invoke_from=self.application_generate_entity.invoke_from,
|
||||
call_depth=self.application_generate_entity.call_depth,
|
||||
variable_pool=variable_pool,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
|
||||
@@ -73,15 +73,9 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
||||
"""
|
||||
app_config = self.application_generate_entity.app_config
|
||||
app_config = cast(PipelineConfig, app_config)
|
||||
invoke_from = self.application_generate_entity.invoke_from
|
||||
|
||||
if self.application_generate_entity.single_iteration_run or self.application_generate_entity.single_loop_run:
|
||||
invoke_from = InvokeFrom.DEBUGGER
|
||||
|
||||
user_from = self._resolve_user_from(invoke_from)
|
||||
|
||||
user_id = None
|
||||
if invoke_from in {InvokeFrom.WEB_APP, InvokeFrom.SERVICE_API}:
|
||||
if self.application_generate_entity.invoke_from in {InvokeFrom.WEB_APP, InvokeFrom.SERVICE_API}:
|
||||
end_user = db.session.query(EndUser).where(EndUser.id == self.application_generate_entity.user_id).first()
|
||||
if end_user:
|
||||
user_id = end_user.session_id
|
||||
@@ -123,7 +117,7 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
||||
dataset_id=self.application_generate_entity.dataset_id,
|
||||
datasource_type=self.application_generate_entity.datasource_type,
|
||||
datasource_info=self.application_generate_entity.datasource_info,
|
||||
invoke_from=invoke_from.value,
|
||||
invoke_from=self.application_generate_entity.invoke_from.value,
|
||||
)
|
||||
|
||||
rag_pipeline_variables = []
|
||||
@@ -155,8 +149,6 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
start_node_id=self.application_generate_entity.start_node_id,
|
||||
workflow=workflow,
|
||||
user_from=user_from,
|
||||
invoke_from=invoke_from,
|
||||
)
|
||||
|
||||
# RUN WORKFLOW
|
||||
@@ -167,8 +159,12 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
||||
graph=graph,
|
||||
graph_config=workflow.graph_dict,
|
||||
user_id=self.application_generate_entity.user_id,
|
||||
user_from=user_from,
|
||||
invoke_from=invoke_from,
|
||||
user_from=(
|
||||
UserFrom.ACCOUNT
|
||||
if self.application_generate_entity.invoke_from in {InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER}
|
||||
else UserFrom.END_USER
|
||||
),
|
||||
invoke_from=self.application_generate_entity.invoke_from,
|
||||
call_depth=self.application_generate_entity.call_depth,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
variable_pool=variable_pool,
|
||||
@@ -214,12 +210,7 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
||||
return workflow
|
||||
|
||||
def _init_rag_pipeline_graph(
|
||||
self,
|
||||
workflow: Workflow,
|
||||
graph_runtime_state: GraphRuntimeState,
|
||||
start_node_id: str | None = None,
|
||||
user_from: UserFrom = UserFrom.ACCOUNT,
|
||||
invoke_from: InvokeFrom = InvokeFrom.SERVICE_API,
|
||||
self, workflow: Workflow, graph_runtime_state: GraphRuntimeState, start_node_id: str | None = None
|
||||
) -> Graph:
|
||||
"""
|
||||
Init pipeline graph
|
||||
@@ -262,8 +253,8 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
||||
workflow_id=workflow.id,
|
||||
graph_config=graph_config,
|
||||
user_id=self.application_generate_entity.user_id,
|
||||
user_from=user_from,
|
||||
invoke_from=invoke_from,
|
||||
user_from=UserFrom.ACCOUNT,
|
||||
invoke_from=InvokeFrom.SERVICE_API,
|
||||
call_depth=0,
|
||||
)
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from core.workflow.workflow_entry import WorkflowEntry
|
||||
from extensions.ext_redis import redis_client
|
||||
from extensions.otel import WorkflowAppRunnerHandler, trace_span
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from models.enums import UserFrom
|
||||
from models.workflow import Workflow
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -73,12 +74,7 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
||||
workflow_execution_id=self.application_generate_entity.workflow_execution_id,
|
||||
)
|
||||
|
||||
invoke_from = self.application_generate_entity.invoke_from
|
||||
# if only single iteration or single loop run is requested
|
||||
if self.application_generate_entity.single_iteration_run or self.application_generate_entity.single_loop_run:
|
||||
invoke_from = InvokeFrom.DEBUGGER
|
||||
user_from = self._resolve_user_from(invoke_from)
|
||||
|
||||
if self.application_generate_entity.single_iteration_run or self.application_generate_entity.single_loop_run:
|
||||
graph, variable_pool, graph_runtime_state = self._prepare_single_node_execution(
|
||||
workflow=self._workflow,
|
||||
@@ -106,8 +102,6 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
||||
workflow_id=self._workflow.id,
|
||||
tenant_id=self._workflow.tenant_id,
|
||||
user_id=self.application_generate_entity.user_id,
|
||||
user_from=user_from,
|
||||
invoke_from=invoke_from,
|
||||
root_node_id=self._root_node_id,
|
||||
)
|
||||
|
||||
@@ -126,8 +120,12 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
||||
graph=graph,
|
||||
graph_config=self._workflow.graph_dict,
|
||||
user_id=self.application_generate_entity.user_id,
|
||||
user_from=user_from,
|
||||
invoke_from=invoke_from,
|
||||
user_from=(
|
||||
UserFrom.ACCOUNT
|
||||
if self.application_generate_entity.invoke_from in {InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER}
|
||||
else UserFrom.END_USER
|
||||
),
|
||||
invoke_from=self.application_generate_entity.invoke_from,
|
||||
call_depth=self.application_generate_entity.call_depth,
|
||||
variable_pool=variable_pool,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
|
||||
@@ -77,18 +77,10 @@ class WorkflowBasedAppRunner:
|
||||
self._app_id = app_id
|
||||
self._graph_engine_layers = graph_engine_layers
|
||||
|
||||
@staticmethod
|
||||
def _resolve_user_from(invoke_from: InvokeFrom) -> UserFrom:
|
||||
if invoke_from in {InvokeFrom.EXPLORE, InvokeFrom.DEBUGGER}:
|
||||
return UserFrom.ACCOUNT
|
||||
return UserFrom.END_USER
|
||||
|
||||
def _init_graph(
|
||||
self,
|
||||
graph_config: Mapping[str, Any],
|
||||
graph_runtime_state: GraphRuntimeState,
|
||||
user_from: UserFrom,
|
||||
invoke_from: InvokeFrom,
|
||||
workflow_id: str = "",
|
||||
tenant_id: str = "",
|
||||
user_id: str = "",
|
||||
@@ -113,8 +105,8 @@ class WorkflowBasedAppRunner:
|
||||
workflow_id=workflow_id,
|
||||
graph_config=graph_config,
|
||||
user_id=user_id,
|
||||
user_from=user_from,
|
||||
invoke_from=invoke_from,
|
||||
user_from=UserFrom.ACCOUNT,
|
||||
invoke_from=InvokeFrom.SERVICE_API,
|
||||
call_depth=0,
|
||||
)
|
||||
|
||||
@@ -258,7 +250,7 @@ class WorkflowBasedAppRunner:
|
||||
graph_config=graph_config,
|
||||
user_id="",
|
||||
user_from=UserFrom.ACCOUNT,
|
||||
invoke_from=InvokeFrom.DEBUGGER,
|
||||
invoke_from=InvokeFrom.SERVICE_API,
|
||||
call_depth=0,
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from .create_site_record_when_app_created import handle as handle_create_site_re
|
||||
from .delete_tool_parameters_cache_when_sync_draft_workflow import (
|
||||
handle as handle_delete_tool_parameters_cache_when_sync_draft_workflow,
|
||||
)
|
||||
from .queue_credential_sync_when_tenant_created import handle as handle_queue_credential_sync_when_tenant_created
|
||||
from .sync_plugin_trigger_when_app_created import handle as handle_sync_plugin_trigger_when_app_created
|
||||
from .sync_webhook_when_app_created import handle as handle_sync_webhook_when_app_created
|
||||
from .sync_workflow_schedule_when_app_published import handle as handle_sync_workflow_schedule_when_app_published
|
||||
@@ -30,6 +31,7 @@ __all__ = [
|
||||
"handle_create_installed_app_when_app_created",
|
||||
"handle_create_site_record_when_app_created",
|
||||
"handle_delete_tool_parameters_cache_when_sync_draft_workflow",
|
||||
"handle_queue_credential_sync_when_tenant_created",
|
||||
"handle_sync_plugin_trigger_when_app_created",
|
||||
"handle_sync_webhook_when_app_created",
|
||||
"handle_sync_workflow_schedule_when_app_published",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
from configs import dify_config
|
||||
from events.tenant_event import tenant_was_created
|
||||
from services.enterprise.workspace_sync import WorkspaceSyncService
|
||||
|
||||
|
||||
@tenant_was_created.connect
|
||||
def handle(sender, **kwargs):
|
||||
"""Queue credential sync when a tenant/workspace is created."""
|
||||
# Only queue sync tasks if plugin manager (enterprise feature) is enabled
|
||||
if not dify_config.ENTERPRISE_ENABLED:
|
||||
return
|
||||
|
||||
tenant = sender
|
||||
|
||||
# Determine source from kwargs if available, otherwise use generic
|
||||
source = kwargs.get("source", "tenant_created")
|
||||
|
||||
# Queue credential sync task to Redis for enterprise backend to process
|
||||
WorkspaceSyncService.queue_credential_sync(tenant.id, source=source)
|
||||
58
api/services/enterprise/workspace_sync.py
Normal file
58
api/services/enterprise/workspace_sync.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from redis import RedisError
|
||||
|
||||
from extensions.ext_redis import redis_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
WORKSPACE_SYNC_QUEUE = "enterprise:workspace:sync:queue"
|
||||
WORKSPACE_SYNC_PROCESSING = "enterprise:workspace:sync:processing"
|
||||
|
||||
|
||||
class WorkspaceSyncService:
|
||||
"""Service to publish workspace sync tasks to Redis queue for enterprise backend consumption"""
|
||||
|
||||
@staticmethod
|
||||
def queue_credential_sync(workspace_id: str, *, source: str) -> bool:
|
||||
"""
|
||||
Queue a credential sync task for a newly created workspace.
|
||||
|
||||
This publishes a task to Redis that will be consumed by the enterprise backend
|
||||
worker to sync credentials with the plugin-manager.
|
||||
|
||||
Args:
|
||||
workspace_id: The workspace/tenant ID to sync credentials for
|
||||
source: Source of the sync request (for debugging/tracking)
|
||||
|
||||
Returns:
|
||||
bool: True if task was queued successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
task = {
|
||||
"task_id": str(uuid.uuid4()),
|
||||
"workspace_id": workspace_id,
|
||||
"retry_count": 0,
|
||||
"created_at": datetime.now(UTC).isoformat(),
|
||||
"source": source,
|
||||
}
|
||||
|
||||
# Push to Redis list (queue) - LPUSH adds to the head, worker consumes from tail with RPOP
|
||||
redis_client.lpush(WORKSPACE_SYNC_QUEUE, json.dumps(task))
|
||||
|
||||
logger.info(
|
||||
"Queued credential sync task for workspace %s, task_id: %s, source: %s",
|
||||
workspace_id,
|
||||
task["task_id"],
|
||||
source,
|
||||
)
|
||||
return True
|
||||
|
||||
except (RedisError, TypeError) as e:
|
||||
logger.error("Failed to queue credential sync for workspace %s: %s", workspace_id, str(e), exc_info=True)
|
||||
# Don't raise - we don't want to fail workspace creation if queueing fails
|
||||
# The scheduled task will catch it later
|
||||
return False
|
||||
@@ -799,7 +799,7 @@ class TriggerProviderService:
|
||||
user_id: str,
|
||||
provider_id: TriggerProviderID,
|
||||
subscription_id: str,
|
||||
credentials: dict[str, Any],
|
||||
credentials: Mapping[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Verify credentials for an existing subscription without updating it.
|
||||
|
||||
@@ -8,12 +8,12 @@ import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Countdown from '@/app/components/signin/countdown'
|
||||
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { sendWebAppEMailLoginCode, webAppEmailLoginWithCode } from '@/service/common'
|
||||
import { fetchAccessToken } from '@/service/share'
|
||||
import { setWebAppAccessToken, setWebAppPassport } from '@/service/webapp-auth'
|
||||
import { encryptVerificationCode } from '@/utils/encryption'
|
||||
|
||||
export default function CheckCode() {
|
||||
const { t } = useTranslation()
|
||||
@@ -64,7 +64,7 @@ export default function CheckCode() {
|
||||
return
|
||||
}
|
||||
setIsLoading(true)
|
||||
const ret = await webAppEmailLoginWithCode({ email, code: encryptVerificationCode(code), token })
|
||||
const ret = await webAppEmailLoginWithCode({ email, code, token })
|
||||
if (ret.result === 'success') {
|
||||
setWebAppAccessToken(ret.data.access_token)
|
||||
const { access_token } = await fetchAccessToken({
|
||||
|
||||
@@ -13,7 +13,6 @@ import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { webAppLogin } from '@/service/common'
|
||||
import { fetchAccessToken } from '@/service/share'
|
||||
import { setWebAppAccessToken, setWebAppPassport } from '@/service/webapp-auth'
|
||||
import { encryptPassword } from '@/utils/encryption'
|
||||
|
||||
type MailAndPasswordAuthProps = {
|
||||
isEmailSetup: boolean
|
||||
@@ -72,7 +71,7 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
|
||||
setIsLoading(true)
|
||||
const loginData: Record<string, any> = {
|
||||
email,
|
||||
password: encryptPassword(password),
|
||||
password,
|
||||
language: locale,
|
||||
remember_me: true,
|
||||
}
|
||||
|
||||
@@ -274,9 +274,9 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
|
||||
</div>
|
||||
)}
|
||||
{hasVar && (
|
||||
<div className={cn('mt-1 grid px-3 pb-3')}>
|
||||
<div className="mt-1 px-3 pb-3">
|
||||
<ReactSortable
|
||||
className={cn('grid-col-1 grid space-y-1', readonly && 'grid-cols-2 gap-1 space-y-0')}
|
||||
className="space-y-1"
|
||||
list={promptVariablesWithIds}
|
||||
setList={(list) => { onPromptVariablesChange?.(list.map(item => item.variable)) }}
|
||||
handle=".handle"
|
||||
|
||||
@@ -39,7 +39,7 @@ const VarItem: FC<ItemProps> = ({
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
|
||||
return (
|
||||
<div className={cn('group relative mb-1 flex h-[34px] w-full items-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg pl-2.5 pr-3 shadow-xs last-of-type:mb-0 hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm', isDeleting && 'border-state-destructive-border hover:bg-state-destructive-hover', readonly && 'cursor-not-allowed', className)}>
|
||||
<div className={cn('group relative mb-1 flex h-[34px] w-full items-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg pl-2.5 pr-3 shadow-xs last-of-type:mb-0 hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm', isDeleting && 'border-state-destructive-border hover:bg-state-destructive-hover', readonly && 'cursor-not-allowed opacity-30', className)}>
|
||||
<VarIcon className={cn('mr-1 h-4 w-4 shrink-0 text-text-accent', canDrag && 'group-hover:opacity-0')} />
|
||||
{canDrag && (
|
||||
<RiDraggable className="absolute left-3 top-3 hidden h-3 w-3 cursor-pointer text-text-tertiary group-hover:block" />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { produce } from 'immer'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
@@ -11,17 +10,14 @@ import { useFeatures, useFeaturesStore } from '@/app/components/base/features/ho
|
||||
import { Vision } from '@/app/components/base/icons/src/vender/features'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
// import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import { Resolution } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import ParamConfig from './param-config'
|
||||
|
||||
const ConfigVision: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { isShowVisionConfig, isAllowVideoUpload, readonly } = useContext(ConfigContext)
|
||||
const { isShowVisionConfig, isAllowVideoUpload } = useContext(ConfigContext)
|
||||
const file = useFeatures(s => s.features.file)
|
||||
const featuresStore = useFeaturesStore()
|
||||
|
||||
@@ -58,7 +54,7 @@ const ConfigVision: FC = () => {
|
||||
setFeatures(newFeatures)
|
||||
}, [featuresStore, isAllowVideoUpload])
|
||||
|
||||
if (!isShowVisionConfig || (readonly && !isImageEnabled))
|
||||
if (!isShowVisionConfig)
|
||||
return null
|
||||
|
||||
return (
|
||||
@@ -79,55 +75,37 @@ const ConfigVision: FC = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center">
|
||||
{readonly
|
||||
? (
|
||||
<>
|
||||
<div className="mr-2 flex items-center gap-0.5">
|
||||
<div className="system-xs-medium-uppercase text-text-tertiary">{t('vision.visionSettings.resolution', { ns: 'appDebug' })}</div>
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="w-[180px]">
|
||||
{t('vision.visionSettings.resolutionTooltip', { ns: 'appDebug' }).split('\n').map(item => (
|
||||
<div key={item}>{item}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<OptionCard
|
||||
title={t('vision.visionSettings.high', { ns: 'appDebug' })}
|
||||
selected={file?.image?.detail === Resolution.high}
|
||||
onSelect={noop}
|
||||
className={cn(
|
||||
'cursor-not-allowed rounded-lg px-3 hover:shadow-none',
|
||||
file?.image?.detail !== Resolution.high && 'hover:border-components-option-card-option-border',
|
||||
)}
|
||||
/>
|
||||
<OptionCard
|
||||
title={t('vision.visionSettings.low', { ns: 'appDebug' })}
|
||||
selected={file?.image?.detail === Resolution.low}
|
||||
onSelect={noop}
|
||||
className={cn(
|
||||
'cursor-not-allowed rounded-lg px-3 hover:shadow-none',
|
||||
file?.image?.detail !== Resolution.low && 'hover:border-components-option-card-option-border',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<ParamConfig />
|
||||
<div className="ml-1 mr-3 h-3.5 w-[1px] bg-divider-regular"></div>
|
||||
<Switch
|
||||
defaultValue={isImageEnabled}
|
||||
onChange={handleChange}
|
||||
size="md"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* <div className='mr-2 flex items-center gap-0.5'>
|
||||
<div className='text-text-tertiary system-xs-medium-uppercase'>{t('appDebug.vision.visionSettings.resolution')}</div>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className='w-[180px]' >
|
||||
{t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => (
|
||||
<div key={item}>{item}</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div> */}
|
||||
{/* <div className='flex items-center gap-1'>
|
||||
<OptionCard
|
||||
title={t('appDebug.vision.visionSettings.high')}
|
||||
selected={file?.image?.detail === Resolution.high}
|
||||
onSelect={() => handleChange(Resolution.high)}
|
||||
/>
|
||||
<OptionCard
|
||||
title={t('appDebug.vision.visionSettings.low')}
|
||||
selected={file?.image?.detail === Resolution.low}
|
||||
onSelect={() => handleChange(Resolution.low)}
|
||||
/>
|
||||
</div> */}
|
||||
<ParamConfig />
|
||||
<div className="ml-1 mr-3 h-3.5 w-[1px] bg-divider-regular"></div>
|
||||
<Switch
|
||||
defaultValue={isImageEnabled}
|
||||
onChange={handleChange}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -40,7 +40,7 @@ type AgentToolWithMoreInfo = AgentTool & { icon: any, collection?: Collection }
|
||||
const AgentTools: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [isShowChooseTool, setIsShowChooseTool] = useState(false)
|
||||
const { readonly, modelConfig, setModelConfig } = useContext(ConfigContext)
|
||||
const { modelConfig, setModelConfig } = useContext(ConfigContext)
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
@@ -168,10 +168,10 @@ const AgentTools: FC = () => {
|
||||
{tools.filter(item => !!item.enabled).length}
|
||||
/
|
||||
{tools.length}
|
||||
|
||||
|
||||
{t('agent.tools.enabled', { ns: 'appDebug' })}
|
||||
</div>
|
||||
{tools.length < MAX_TOOLS_NUM && !readonly && (
|
||||
{tools.length < MAX_TOOLS_NUM && (
|
||||
<>
|
||||
<div className="ml-3 mr-1 h-3.5 w-px bg-divider-regular"></div>
|
||||
<ToolPicker
|
||||
@@ -190,7 +190,7 @@ const AgentTools: FC = () => {
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className={cn('grid grid-cols-1 flex-wrap items-center justify-between gap-1 2xl:grid-cols-2', readonly && 'grid-cols-2')}>
|
||||
<div className="grid grid-cols-1 flex-wrap items-center justify-between gap-1 2xl:grid-cols-2">
|
||||
{tools.map((item: AgentTool & { icon: any, collection?: Collection }, index) => (
|
||||
<div
|
||||
key={index}
|
||||
@@ -215,7 +215,7 @@ const AgentTools: FC = () => {
|
||||
>
|
||||
<span className="system-xs-medium pr-1.5 text-text-secondary">{getProviderShowName(item)}</span>
|
||||
<span className="text-text-tertiary">{item.tool_label}</span>
|
||||
{!item.isDeleted && !readonly && (
|
||||
{!item.isDeleted && (
|
||||
<Tooltip
|
||||
popupContent={(
|
||||
<div className="w-[180px]">
|
||||
@@ -260,7 +260,7 @@ const AgentTools: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!item.isDeleted && !readonly && (
|
||||
{!item.isDeleted && (
|
||||
<div className="mr-2 hidden items-center gap-1 group-hover:flex">
|
||||
{!item.notAuthor && (
|
||||
<Tooltip
|
||||
@@ -299,7 +299,7 @@ const AgentTools: FC = () => {
|
||||
{!item.notAuthor && (
|
||||
<Switch
|
||||
defaultValue={item.isDeleted ? false : item.enabled}
|
||||
disabled={item.isDeleted || readonly}
|
||||
disabled={item.isDeleted}
|
||||
size="md"
|
||||
onChange={(enabled) => {
|
||||
const newModelConfig = produce(modelConfig, (draft) => {
|
||||
@@ -313,7 +313,6 @@ const AgentTools: FC = () => {
|
||||
{item.notAuthor && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={readonly}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setCurrentTool(item)
|
||||
|
||||
@@ -17,7 +17,7 @@ const ConfigAudio: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const file = useFeatures(s => s.features.file)
|
||||
const featuresStore = useFeaturesStore()
|
||||
const { isShowAudioConfig, readonly } = useContext(ConfigContext)
|
||||
const { isShowAudioConfig } = useContext(ConfigContext)
|
||||
|
||||
const isAudioEnabled = file?.allowed_file_types?.includes(SupportUploadFileTypes.audio) ?? false
|
||||
|
||||
@@ -45,7 +45,7 @@ const ConfigAudio: FC = () => {
|
||||
setFeatures(newFeatures)
|
||||
}, [featuresStore])
|
||||
|
||||
if (!isShowAudioConfig || (readonly && !isAudioEnabled))
|
||||
if (!isShowAudioConfig)
|
||||
return null
|
||||
|
||||
return (
|
||||
@@ -65,16 +65,14 @@ const ConfigAudio: FC = () => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{!readonly && (
|
||||
<div className="flex shrink-0 items-center">
|
||||
<div className="ml-1 mr-3 h-3.5 w-[1px] bg-divider-subtle"></div>
|
||||
<Switch
|
||||
defaultValue={isAudioEnabled}
|
||||
onChange={handleChange}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex shrink-0 items-center">
|
||||
<div className="ml-1 mr-3 h-3.5 w-[1px] bg-divider-subtle"></div>
|
||||
<Switch
|
||||
defaultValue={isAudioEnabled}
|
||||
onChange={handleChange}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ const ConfigDocument: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const file = useFeatures(s => s.features.file)
|
||||
const featuresStore = useFeaturesStore()
|
||||
const { isShowDocumentConfig, readonly } = useContext(ConfigContext)
|
||||
const { isShowDocumentConfig } = useContext(ConfigContext)
|
||||
|
||||
const isDocumentEnabled = file?.allowed_file_types?.includes(SupportUploadFileTypes.document) ?? false
|
||||
|
||||
@@ -45,7 +45,7 @@ const ConfigDocument: FC = () => {
|
||||
setFeatures(newFeatures)
|
||||
}, [featuresStore])
|
||||
|
||||
if (!isShowDocumentConfig || (readonly && !isDocumentEnabled))
|
||||
if (!isShowDocumentConfig)
|
||||
return null
|
||||
|
||||
return (
|
||||
@@ -65,16 +65,14 @@ const ConfigDocument: FC = () => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{!readonly && (
|
||||
<div className="flex shrink-0 items-center">
|
||||
<div className="ml-1 mr-3 h-3.5 w-[1px] bg-divider-subtle"></div>
|
||||
<Switch
|
||||
defaultValue={isDocumentEnabled}
|
||||
onChange={handleChange}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex shrink-0 items-center">
|
||||
<div className="ml-1 mr-3 h-3.5 w-[1px] bg-divider-subtle"></div>
|
||||
<Switch
|
||||
defaultValue={isDocumentEnabled}
|
||||
onChange={handleChange}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import ConfigDocument from './config-document'
|
||||
|
||||
const Config: FC = () => {
|
||||
const {
|
||||
readonly,
|
||||
mode,
|
||||
isAdvancedMode,
|
||||
modelModeType,
|
||||
@@ -28,7 +27,6 @@ const Config: FC = () => {
|
||||
modelConfig,
|
||||
setModelConfig,
|
||||
setPrevPromptConfig,
|
||||
dataSets,
|
||||
} = useContext(ConfigContext)
|
||||
const isChatApp = [AppModeEnum.ADVANCED_CHAT, AppModeEnum.AGENT_CHAT, AppModeEnum.CHAT].includes(mode)
|
||||
const formattingChangedDispatcher = useFormattingChangedDispatcher()
|
||||
@@ -67,27 +65,19 @@ const Config: FC = () => {
|
||||
promptTemplate={promptTemplate}
|
||||
promptVariables={promptVariables}
|
||||
onChange={handlePromptChange}
|
||||
readonly={readonly}
|
||||
/>
|
||||
|
||||
{/* Variables */}
|
||||
{!(readonly && promptVariables.length === 0) && (
|
||||
<ConfigVar
|
||||
promptVariables={promptVariables}
|
||||
onPromptVariablesChange={handlePromptVariablesNameChange}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)}
|
||||
<ConfigVar
|
||||
promptVariables={promptVariables}
|
||||
onPromptVariablesChange={handlePromptVariablesNameChange}
|
||||
/>
|
||||
|
||||
{/* Dataset */}
|
||||
{!(readonly && dataSets.length === 0) && (
|
||||
<DatasetConfig
|
||||
readonly={readonly}
|
||||
hideMetadataFilter={readonly}
|
||||
/>
|
||||
)}
|
||||
<DatasetConfig />
|
||||
|
||||
{/* Tools */}
|
||||
{isAgent && !(readonly && modelConfig.agentConfig.tools.length === 0) && (
|
||||
{isAgent && (
|
||||
<AgentTools />
|
||||
)}
|
||||
|
||||
@@ -98,7 +88,7 @@ const Config: FC = () => {
|
||||
<ConfigAudio />
|
||||
|
||||
{/* Chat History */}
|
||||
{!readonly && isAdvancedMode && isChatApp && modelModeType === ModelModeType.completion && (
|
||||
{isAdvancedMode && isChatApp && modelModeType === ModelModeType.completion && (
|
||||
<HistoryPanel
|
||||
showWarning={!hasSetBlockStatus.history}
|
||||
onShowEditModal={showHistoryModal}
|
||||
|
||||
@@ -30,7 +30,6 @@ const Item: FC<ItemProps> = ({
|
||||
config,
|
||||
onSave,
|
||||
onRemove,
|
||||
readonly = false,
|
||||
editable = true,
|
||||
}) => {
|
||||
const media = useBreakpoints()
|
||||
@@ -57,7 +56,6 @@ const Item: FC<ItemProps> = ({
|
||||
<div className={cn(
|
||||
'group relative mb-1 flex h-10 w-full cursor-pointer items-center justify-between rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-2 last-of-type:mb-0 hover:bg-components-panel-on-panel-item-bg-hover',
|
||||
isDeleting && 'border-state-destructive-border hover:bg-state-destructive-hover',
|
||||
readonly && 'cursor-not-allowed',
|
||||
)}
|
||||
>
|
||||
<div className="flex w-0 grow items-center space-x-1.5">
|
||||
@@ -72,7 +70,7 @@ const Item: FC<ItemProps> = ({
|
||||
</div>
|
||||
<div className="ml-2 hidden shrink-0 items-center space-x-1 group-hover:flex">
|
||||
{
|
||||
editable && !readonly && (
|
||||
editable && (
|
||||
<ActionButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
@@ -83,18 +81,14 @@ const Item: FC<ItemProps> = ({
|
||||
</ActionButton>
|
||||
)
|
||||
}
|
||||
{
|
||||
!readonly && (
|
||||
<ActionButton
|
||||
onClick={() => onRemove(config.id)}
|
||||
state={isDeleting ? ActionButtonState.Destructive : ActionButtonState.Default}
|
||||
onMouseEnter={() => setIsDeleting(true)}
|
||||
onMouseLeave={() => setIsDeleting(false)}
|
||||
>
|
||||
<RiDeleteBinLine className={cn('h-4 w-4 shrink-0 text-text-tertiary', isDeleting && 'text-text-destructive')} />
|
||||
</ActionButton>
|
||||
)
|
||||
}
|
||||
<ActionButton
|
||||
onClick={() => onRemove(config.id)}
|
||||
state={isDeleting ? ActionButtonState.Destructive : ActionButtonState.Default}
|
||||
onMouseEnter={() => setIsDeleting(true)}
|
||||
onMouseLeave={() => setIsDeleting(false)}
|
||||
>
|
||||
<RiDeleteBinLine className={cn('h-4 w-4 shrink-0 text-text-tertiary', isDeleting && 'text-text-destructive')} />
|
||||
</ActionButton>
|
||||
</div>
|
||||
{
|
||||
config.indexing_technique && (
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
import { useSelector as useAppContextSelector } from '@/context/app-context'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { hasEditPermissionForDataset } from '@/utils/permission'
|
||||
import FeaturePanel from '../base/feature-panel'
|
||||
import OperationBtn from '../base/operation-btn'
|
||||
@@ -39,11 +38,7 @@ import CardItem from './card-item'
|
||||
import ContextVar from './context-var'
|
||||
import ParamsConfig from './params-config'
|
||||
|
||||
type Props = {
|
||||
readonly?: boolean
|
||||
hideMetadataFilter?: boolean
|
||||
}
|
||||
const DatasetConfig: FC<Props> = ({ readonly, hideMetadataFilter }) => {
|
||||
const DatasetConfig: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const userProfile = useAppContextSelector(s => s.userProfile)
|
||||
const {
|
||||
@@ -264,19 +259,17 @@ const DatasetConfig: FC<Props> = ({ readonly, hideMetadataFilter }) => {
|
||||
className="mt-2"
|
||||
title={t('feature.dataSet.title', { ns: 'appDebug' })}
|
||||
headerRight={(
|
||||
!readonly && (
|
||||
<div className="flex items-center gap-1">
|
||||
{!isAgent && <ParamsConfig disabled={!hasData} selectedDatasets={dataSet} />}
|
||||
<OperationBtn type="add" onClick={showSelectDataSet} />
|
||||
</div>
|
||||
)
|
||||
<div className="flex items-center gap-1">
|
||||
{!isAgent && <ParamsConfig disabled={!hasData} selectedDatasets={dataSet} />}
|
||||
<OperationBtn type="add" onClick={showSelectDataSet} />
|
||||
</div>
|
||||
)}
|
||||
hasHeaderBottomBorder={!hasData}
|
||||
noBodySpacing
|
||||
>
|
||||
{hasData
|
||||
? (
|
||||
<div className={cn('mt-1 flex flex-wrap justify-between px-3 pb-3', readonly && 'grid-cols-2 gap-1')}>
|
||||
<div className="mt-1 flex flex-wrap justify-between px-3 pb-3">
|
||||
{formattedDataset.map(item => (
|
||||
<CardItem
|
||||
key={item.id}
|
||||
@@ -294,29 +287,27 @@ const DatasetConfig: FC<Props> = ({ readonly, hideMetadataFilter }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hideMetadataFilter && (
|
||||
<div className="border-t border-t-divider-subtle py-2">
|
||||
<MetadataFilter
|
||||
metadataList={metadataList}
|
||||
selectedDatasetsLoaded
|
||||
metadataFilterMode={datasetConfigs.metadata_filtering_mode}
|
||||
metadataFilteringConditions={datasetConfigs.metadata_filtering_conditions}
|
||||
handleAddCondition={handleAddCondition}
|
||||
handleMetadataFilterModeChange={handleMetadataFilterModeChange}
|
||||
handleRemoveCondition={handleRemoveCondition}
|
||||
handleToggleConditionLogicalOperator={handleToggleConditionLogicalOperator}
|
||||
handleUpdateCondition={handleUpdateCondition}
|
||||
metadataModelConfig={datasetConfigs.metadata_model_config}
|
||||
handleMetadataModelChange={handleMetadataModelChange}
|
||||
handleMetadataCompletionParamsChange={handleMetadataCompletionParamsChange}
|
||||
isCommonVariable
|
||||
availableCommonStringVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.string || item.type === MetadataFilteringVariableType.select)}
|
||||
availableCommonNumberVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.number)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t border-t-divider-subtle py-2">
|
||||
<MetadataFilter
|
||||
metadataList={metadataList}
|
||||
selectedDatasetsLoaded
|
||||
metadataFilterMode={datasetConfigs.metadata_filtering_mode}
|
||||
metadataFilteringConditions={datasetConfigs.metadata_filtering_conditions}
|
||||
handleAddCondition={handleAddCondition}
|
||||
handleMetadataFilterModeChange={handleMetadataFilterModeChange}
|
||||
handleRemoveCondition={handleRemoveCondition}
|
||||
handleToggleConditionLogicalOperator={handleToggleConditionLogicalOperator}
|
||||
handleUpdateCondition={handleUpdateCondition}
|
||||
metadataModelConfig={datasetConfigs.metadata_model_config}
|
||||
handleMetadataModelChange={handleMetadataModelChange}
|
||||
handleMetadataCompletionParamsChange={handleMetadataCompletionParamsChange}
|
||||
isCommonVariable
|
||||
availableCommonStringVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.string || item.type === MetadataFilteringVariableType.select)}
|
||||
availableCommonNumberVars={promptVariablesToSelect.filter(item => item.type === MetadataFilteringVariableType.number)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!readonly && mode === AppModeEnum.COMPLETION && dataSet.length > 0 && (
|
||||
{mode === AppModeEnum.COMPLETION && dataSet.length > 0 && (
|
||||
<ContextVar
|
||||
value={selectedContextVar?.key}
|
||||
options={promptVariablesToSelect}
|
||||
|
||||
@@ -19,7 +19,7 @@ const ChatUserInput = ({
|
||||
inputs,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { modelConfig, setInputs, readonly } = useContext(ConfigContext)
|
||||
const { modelConfig, setInputs } = useContext(ConfigContext)
|
||||
|
||||
const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => {
|
||||
return key && key?.trim() && name && name?.trim()
|
||||
@@ -89,7 +89,6 @@ const ChatUserInput = ({
|
||||
placeholder={name}
|
||||
autoFocus={index === 0}
|
||||
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
{type === 'paragraph' && (
|
||||
@@ -98,7 +97,6 @@ const ChatUserInput = ({
|
||||
placeholder={name}
|
||||
value={inputs[key] ? `${inputs[key]}` : ''}
|
||||
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
{type === 'select' && (
|
||||
@@ -108,7 +106,6 @@ const ChatUserInput = ({
|
||||
onSelect={(i) => { handleInputValueChange(key, i.value as string) }}
|
||||
items={(options || []).map(i => ({ name: i, value: i }))}
|
||||
allowSearch={false}
|
||||
disabled={readonly}
|
||||
/>
|
||||
)}
|
||||
{type === 'number' && (
|
||||
@@ -119,7 +116,6 @@ const ChatUserInput = ({
|
||||
placeholder={name}
|
||||
autoFocus={index === 0}
|
||||
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
{type === 'checkbox' && (
|
||||
@@ -128,7 +124,6 @@ const ChatUserInput = ({
|
||||
value={!!inputs[key]}
|
||||
required={required}
|
||||
onChange={(value) => { handleInputValueChange(key, value) }}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,6 @@ import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/
|
||||
import { useDebugConfigurationContext } from '@/context/debug-configuration'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
|
||||
import { APP_CHAT_WITH_MULTIPLE_MODEL } from '../types'
|
||||
|
||||
@@ -131,11 +130,11 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
|
||||
|
||||
return (
|
||||
<TextGeneration
|
||||
appSourceType={AppSourceType.webApp}
|
||||
className="flex h-full flex-col overflow-y-auto border-none"
|
||||
content={completion}
|
||||
isLoading={!completion && isResponding}
|
||||
isResponding={isResponding}
|
||||
isInstalledApp={false}
|
||||
siteInfo={null}
|
||||
messageId={messageId}
|
||||
isError={false}
|
||||
|
||||
@@ -39,7 +39,6 @@ const DebugWithSingleModel = (
|
||||
) => {
|
||||
const { userProfile } = useAppContext()
|
||||
const {
|
||||
readonly,
|
||||
modelConfig,
|
||||
appId,
|
||||
inputs,
|
||||
@@ -151,7 +150,6 @@ const DebugWithSingleModel = (
|
||||
|
||||
return (
|
||||
<Chat
|
||||
readonly={readonly}
|
||||
config={config}
|
||||
chatList={chatList}
|
||||
isResponding={isResponding}
|
||||
|
||||
@@ -38,7 +38,6 @@ import ConfigContext from '@/context/debug-configuration'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { sendCompletionMessage } from '@/service/debug'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
import { AppModeEnum, ModelModeType, TransferMethod } from '@/types/app'
|
||||
import { formatBooleanInputs, promptVariablesToUserInputsForm } from '@/utils/model-config'
|
||||
import GroupName from '../base/group-name'
|
||||
@@ -73,7 +72,6 @@ const Debug: FC<IDebug> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
readonly,
|
||||
appId,
|
||||
mode,
|
||||
modelModeType,
|
||||
@@ -418,33 +416,25 @@ const Debug: FC<IDebug> = ({
|
||||
}
|
||||
{mode !== AppModeEnum.COMPLETION && (
|
||||
<>
|
||||
{
|
||||
!readonly && (
|
||||
<TooltipPlus
|
||||
popupContent={t('operation.refresh', { ns: 'common' })}
|
||||
>
|
||||
<ActionButton onClick={clearConversation}>
|
||||
<RefreshCcw01 className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</TooltipPlus>
|
||||
{varList.length > 0 && (
|
||||
<div className="relative ml-1 mr-2">
|
||||
<TooltipPlus
|
||||
popupContent={t('operation.refresh', { ns: 'common' })}
|
||||
popupContent={t('panel.userInputField', { ns: 'workflow' })}
|
||||
>
|
||||
<ActionButton onClick={clearConversation}>
|
||||
<RefreshCcw01 className="h-4 w-4" />
|
||||
<ActionButton state={expanded ? ActionButtonState.Active : undefined} onClick={() => setExpanded(!expanded)}>
|
||||
<RiEqualizer2Line className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
|
||||
</TooltipPlus>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
varList.length > 0 && (
|
||||
<div className="relative ml-1 mr-2">
|
||||
<TooltipPlus
|
||||
popupContent={t('panel.userInputField', { ns: 'workflow' })}
|
||||
>
|
||||
<ActionButton state={expanded ? ActionButtonState.Active : undefined} onClick={() => !readonly && setExpanded(!expanded)}>
|
||||
<RiEqualizer2Line className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</TooltipPlus>
|
||||
{expanded && <div className="absolute bottom-[-14px] right-[5px] z-10 h-3 w-3 rotate-45 border-l-[0.5px] border-t-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg" />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{expanded && <div className="absolute bottom-[-14px] right-[5px] z-10 h-3 w-3 rotate-45 border-l-[0.5px] border-t-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg" />}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -454,21 +444,19 @@ const Debug: FC<IDebug> = ({
|
||||
<ChatUserInput inputs={inputs} />
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
mode === AppModeEnum.COMPLETION && (
|
||||
<PromptValuePanel
|
||||
appType={mode as AppModeEnum}
|
||||
onSend={handleSendTextCompletion}
|
||||
inputs={inputs}
|
||||
visionConfig={{
|
||||
...features.file! as VisionSettings,
|
||||
transfer_methods: features.file!.allowed_file_upload_methods || [],
|
||||
image_file_size_limit: features.file?.fileUploadConfig?.image_file_size_limit,
|
||||
}}
|
||||
onVisionFilesChange={setCompletionFiles}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{mode === AppModeEnum.COMPLETION && (
|
||||
<PromptValuePanel
|
||||
appType={mode as AppModeEnum}
|
||||
onSend={handleSendTextCompletion}
|
||||
inputs={inputs}
|
||||
visionConfig={{
|
||||
...features.file! as VisionSettings,
|
||||
transfer_methods: features.file!.allowed_file_upload_methods || [],
|
||||
image_file_size_limit: features.file?.fileUploadConfig?.image_file_size_limit,
|
||||
}}
|
||||
onVisionFilesChange={setCompletionFiles}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{
|
||||
debugWithMultipleModel && (
|
||||
@@ -522,12 +510,12 @@ const Debug: FC<IDebug> = ({
|
||||
<div className="mx-4 mt-3"><GroupName name={t('result', { ns: 'appDebug' })} /></div>
|
||||
<div className="mx-3 mb-8">
|
||||
<TextGeneration
|
||||
appSourceType={AppSourceType.webApp}
|
||||
className="mt-2"
|
||||
content={completionRes}
|
||||
isLoading={!completionRes && isResponding}
|
||||
isShowTextToSpeech={textToSpeechConfig.enabled && !!text2speechDefaultModel}
|
||||
isResponding={isResponding}
|
||||
isInstalledApp={false}
|
||||
messageId={messageId}
|
||||
isError={false}
|
||||
onRetry={noop}
|
||||
@@ -562,15 +550,13 @@ const Debug: FC<IDebug> = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
isShowFormattingChangeConfirm && (
|
||||
<FormattingChanged
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{!isAPIKeySet && !readonly && (<HasNotSetAPIKEY isTrailFinished={!IS_CE_EDITION} onSetting={onSetting} />)}
|
||||
{isShowFormattingChangeConfirm && (
|
||||
<FormattingChanged
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
)}
|
||||
{!isAPIKeySet && (<HasNotSetAPIKEY isTrailFinished={!IS_CE_EDITION} onSetting={onSetting} />)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
onVisionFilesChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { readonly, modelModeType, modelConfig, setInputs, mode, isAdvancedMode, completionPromptConfig, chatPromptConfig } = useContext(ConfigContext)
|
||||
const { modelModeType, modelConfig, setInputs, mode, isAdvancedMode, completionPromptConfig, chatPromptConfig } = useContext(ConfigContext)
|
||||
const [userInputFieldCollapse, setUserInputFieldCollapse] = useState(false)
|
||||
const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => {
|
||||
return key && key?.trim() && name && name?.trim()
|
||||
@@ -79,12 +79,12 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
|
||||
if (isAdvancedMode) {
|
||||
if (modelModeType === ModelModeType.chat)
|
||||
return chatPromptConfig?.prompt.every(({ text }) => !text)
|
||||
return chatPromptConfig.prompt.every(({ text }) => !text)
|
||||
return !completionPromptConfig.prompt?.text
|
||||
}
|
||||
|
||||
else { return !modelConfig.configs.prompt_template }
|
||||
}, [chatPromptConfig?.prompt, completionPromptConfig.prompt?.text, isAdvancedMode, mode, modelConfig.configs.prompt_template, modelModeType])
|
||||
}, [chatPromptConfig.prompt, completionPromptConfig.prompt?.text, isAdvancedMode, mode, modelConfig.configs.prompt_template, modelModeType])
|
||||
|
||||
const handleInputValueChange = (key: string, value: string | boolean) => {
|
||||
if (!(key in promptVariableObj))
|
||||
@@ -143,7 +143,6 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
placeholder={name}
|
||||
autoFocus={index === 0}
|
||||
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
{type === 'paragraph' && (
|
||||
@@ -152,7 +151,6 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
placeholder={name}
|
||||
value={inputs[key] ? `${inputs[key]}` : ''}
|
||||
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
{type === 'select' && (
|
||||
@@ -163,7 +161,6 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
items={(options || []).map(i => ({ name: i, value: i }))}
|
||||
allowSearch={false}
|
||||
bgClassName="bg-gray-50"
|
||||
disabled={readonly}
|
||||
/>
|
||||
)}
|
||||
{type === 'number' && (
|
||||
@@ -174,7 +171,6 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
placeholder={name}
|
||||
autoFocus={index === 0}
|
||||
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
{type === 'checkbox' && (
|
||||
@@ -183,7 +179,6 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
value={!!inputs[key]}
|
||||
required={required}
|
||||
onChange={(value) => { handleInputValueChange(key, value) }}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -202,7 +197,6 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
url: fileItem.url,
|
||||
upload_file_id: fileItem.fileId,
|
||||
})))}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -211,12 +205,12 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
)}
|
||||
{!userInputFieldCollapse && (
|
||||
<div className="flex justify-between border-t border-divider-subtle p-4 pt-3">
|
||||
<Button className="w-[72px]" disabled={readonly} onClick={onClear}>{t('operation.clear', { ns: 'common' })}</Button>
|
||||
<Button className="w-[72px]" onClick={onClear}>{t('operation.clear', { ns: 'common' })}</Button>
|
||||
{canNotRun && (
|
||||
<Tooltip popupContent={t('otherError.promptNoBeEmpty', { ns: 'appDebug' })}>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={canNotRun || readonly}
|
||||
disabled={canNotRun}
|
||||
onClick={() => onSend?.()}
|
||||
className="w-[96px]"
|
||||
>
|
||||
@@ -228,7 +222,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
{!canNotRun && (
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={canNotRun || readonly}
|
||||
disabled={canNotRun}
|
||||
onClick={() => onSend?.()}
|
||||
className="w-[96px]"
|
||||
>
|
||||
@@ -244,8 +238,6 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
showFileUpload={false}
|
||||
isChatMode={appType !== AppModeEnum.COMPLETION}
|
||||
onFeatureBarClick={setShowAppConfigureFeaturesModal}
|
||||
disabled={readonly}
|
||||
hideEditEntrance={readonly}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -10,7 +10,6 @@ vi.mock('@heroicons/react/20/solid', () => ({
|
||||
}))
|
||||
|
||||
const mockApp: App = {
|
||||
can_trial: true,
|
||||
app: {
|
||||
id: 'test-app-id',
|
||||
mode: AppModeEnum.CHAT,
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
'use client'
|
||||
import type { App } from '@/models/explore'
|
||||
import { PlusIcon } from '@heroicons/react/20/solid'
|
||||
import { RiInformation2Line } from '@remixicon/react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContextSelector } from 'use-context-selector'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Button from '@/app/components/base/button'
|
||||
import AppListContext from '@/context/app-list-context'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { AppTypeIcon, AppTypeLabel } from '../../type-selector'
|
||||
|
||||
@@ -25,14 +20,6 @@ const AppCard = ({
|
||||
}: AppCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { app: appBasicInfo } = app
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const isTrialApp = app.can_trial && systemFeatures.enable_trial_app
|
||||
const setShowTryAppPanel = useContextSelector(AppListContext, ctx => ctx.setShowTryAppPanel)
|
||||
const showTryAPPPanel = useCallback((appId: string) => {
|
||||
return () => {
|
||||
setShowTryAppPanel?.(true, { appId, app })
|
||||
}
|
||||
}, [setShowTryAppPanel, app.category])
|
||||
return (
|
||||
<div className={cn('group relative flex h-[132px] cursor-pointer flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-4 shadow-xs hover:shadow-lg')}>
|
||||
<div className="flex shrink-0 grow-0 items-center gap-3 pb-2">
|
||||
@@ -69,12 +56,6 @@ const AppCard = ({
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
<span className="text-xs">{t('newApp.useTemplate', { ns: 'app' })}</span>
|
||||
</Button>
|
||||
{isTrialApp && (
|
||||
<Button className="w-full" onClick={showTryAPPPanel(app.app_id)}>
|
||||
<RiInformation2Line className="mr-1 size-4" />
|
||||
<span>{t('appCard.try', { ns: 'explore' })}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -39,7 +39,6 @@ import { useAppContext } from '@/context/app-context'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import { fetchChatMessages, updateLogMessageAnnotations, updateLogMessageFeedbacks } from '@/service/log'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
import { useChatConversationDetail, useCompletionConversationDetail } from '@/service/use-log'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@@ -703,12 +702,12 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
</div>
|
||||
</div>
|
||||
<TextGeneration
|
||||
appSourceType={AppSourceType.webApp}
|
||||
className="mt-2"
|
||||
content={detail.message.answer}
|
||||
messageId={detail.message.id}
|
||||
isError={false}
|
||||
onRetry={noop}
|
||||
isInstalledApp={false}
|
||||
supportFeedback
|
||||
feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')}
|
||||
onFeedback={feedback => onFeedback(detail.message.id, feedback)}
|
||||
|
||||
@@ -29,7 +29,7 @@ import { Markdown } from '@/app/components/base/markdown'
|
||||
import NewAudioButton from '@/app/components/base/new-audio-button'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { fetchTextGenerationMessage } from '@/service/debug'
|
||||
import { AppSourceType, fetchMoreLikeThis, updateFeedback } from '@/service/share'
|
||||
import { fetchMoreLikeThis, updateFeedback } from '@/service/share'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import ResultTab from './result-tab'
|
||||
|
||||
@@ -53,7 +53,7 @@ export type IGenerationItemProps = {
|
||||
onFeedback?: (feedback: FeedbackType) => void
|
||||
onSave?: (messageId: string) => void
|
||||
isMobile?: boolean
|
||||
appSourceType: AppSourceType
|
||||
isInstalledApp: boolean
|
||||
installedAppId?: string
|
||||
taskId?: string
|
||||
controlClearMoreLikeThis?: number
|
||||
@@ -87,7 +87,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
onSave,
|
||||
depth = 1,
|
||||
isMobile,
|
||||
appSourceType,
|
||||
isInstalledApp,
|
||||
installedAppId,
|
||||
taskId,
|
||||
controlClearMoreLikeThis,
|
||||
@@ -100,7 +100,6 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
const { t } = useTranslation()
|
||||
const params = useParams()
|
||||
const isTop = depth === 1
|
||||
const isTryApp = appSourceType === AppSourceType.tryApp
|
||||
const [completionRes, setCompletionRes] = useState('')
|
||||
const [childMessageId, setChildMessageId] = useState<string | null>(null)
|
||||
const [childFeedback, setChildFeedback] = useState<FeedbackType>({
|
||||
@@ -114,7 +113,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
const setShowPromptLogModal = useAppStore(s => s.setShowPromptLogModal)
|
||||
|
||||
const handleFeedback = async (childFeedback: FeedbackType) => {
|
||||
await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, appSourceType, installedAppId)
|
||||
await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, isInstalledApp, installedAppId)
|
||||
setChildFeedback(childFeedback)
|
||||
}
|
||||
|
||||
@@ -132,7 +131,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
onSave,
|
||||
isShowTextToSpeech,
|
||||
isMobile,
|
||||
appSourceType,
|
||||
isInstalledApp,
|
||||
installedAppId,
|
||||
controlClearMoreLikeThis,
|
||||
isWorkflow,
|
||||
@@ -146,7 +145,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
return
|
||||
}
|
||||
startQuerying()
|
||||
const res: any = await fetchMoreLikeThis(messageId as string, appSourceType, installedAppId)
|
||||
const res: any = await fetchMoreLikeThis(messageId as string, isInstalledApp, installedAppId)
|
||||
setCompletionRes(res.answer)
|
||||
setChildFeedback({
|
||||
rating: null,
|
||||
@@ -311,7 +310,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
)}
|
||||
{/* action buttons */}
|
||||
<div className="absolute bottom-1 right-2 flex items-center">
|
||||
{!isInWebApp && (appSourceType !== AppSourceType.installedApp) && !isResponding && (
|
||||
{!isInWebApp && !isInstalledApp && !isResponding && (
|
||||
<div className="ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm">
|
||||
<ActionButton disabled={isError || !messageId} onClick={handleOpenLogModal}>
|
||||
<RiFileList3Line className="h-4 w-4" />
|
||||
@@ -320,12 +319,12 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
</div>
|
||||
)}
|
||||
<div className="ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm">
|
||||
{moreLikeThis && !isTryApp && (
|
||||
{moreLikeThis && (
|
||||
<ActionButton state={depth === MAX_DEPTH ? ActionButtonState.Disabled : ActionButtonState.Default} disabled={depth === MAX_DEPTH} onClick={handleMoreLikeThis}>
|
||||
<RiSparklingLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
{isShowTextToSpeech && !isTryApp && (
|
||||
{isShowTextToSpeech && (
|
||||
<NewAudioButton
|
||||
id={messageId!}
|
||||
voice={config?.text_to_speech?.voice}
|
||||
@@ -351,13 +350,13 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
<RiReplay15Line className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
{isInWebApp && !isWorkflow && !isTryApp && (
|
||||
{isInWebApp && !isWorkflow && (
|
||||
<ActionButton disabled={isError || !messageId} onClick={() => { onSave?.(messageId as string) }}>
|
||||
<RiBookmark3Line className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
{(supportFeedback || isInWebApp) && !isWorkflow && !isTryApp && !isError && messageId && (
|
||||
{(supportFeedback || isInWebApp) && !isWorkflow && !isError && messageId && (
|
||||
<div className="ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm">
|
||||
{!feedback?.rating && (
|
||||
<>
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
'use client'
|
||||
import type { CreateAppModalProps } from '../explore/create-app-modal'
|
||||
import type { CurrentTryAppParams } from '@/context/explore-context'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEducationInit } from '@/app/education-apply/hooks'
|
||||
import AppListContext from '@/context/app-list-context'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useImportDSL } from '@/hooks/use-import-dsl'
|
||||
import { DSLImportMode } from '@/models/app'
|
||||
import { fetchAppDetail } from '@/service/explore'
|
||||
import DSLConfirmModal from '../app/create-from-dsl-modal/dsl-confirm-modal'
|
||||
import CreateAppModal from '../explore/create-app-modal'
|
||||
import TryApp from '../explore/try-app'
|
||||
import List from './list'
|
||||
|
||||
const Apps = () => {
|
||||
@@ -20,124 +10,10 @@ const Apps = () => {
|
||||
useDocumentTitle(t('menus.apps', { ns: 'common' }))
|
||||
useEducationInit()
|
||||
|
||||
const [currentTryAppParams, setCurrentTryAppParams] = useState<CurrentTryAppParams | undefined>(undefined)
|
||||
const currApp = currentTryAppParams?.app
|
||||
const [isShowTryAppPanel, setIsShowTryAppPanel] = useState(false)
|
||||
const hideTryAppPanel = useCallback(() => {
|
||||
setIsShowTryAppPanel(false)
|
||||
}, [])
|
||||
const setShowTryAppPanel = (showTryAppPanel: boolean, params?: CurrentTryAppParams) => {
|
||||
if (showTryAppPanel)
|
||||
setCurrentTryAppParams(params)
|
||||
else
|
||||
setCurrentTryAppParams(undefined)
|
||||
setIsShowTryAppPanel(showTryAppPanel)
|
||||
}
|
||||
const [isShowCreateModal, setIsShowCreateModal] = useState(false)
|
||||
|
||||
const handleShowFromTryApp = useCallback(() => {
|
||||
setIsShowCreateModal(true)
|
||||
}, [])
|
||||
|
||||
const [controlRefreshList, setControlRefreshList] = useState(0)
|
||||
const [controlHideCreateFromTemplatePanel, setControlHideCreateFromTemplatePanel] = useState(0)
|
||||
const onSuccess = useCallback(() => {
|
||||
setControlRefreshList(prev => prev + 1)
|
||||
setControlHideCreateFromTemplatePanel(prev => prev + 1)
|
||||
}, [])
|
||||
|
||||
const [showDSLConfirmModal, setShowDSLConfirmModal] = useState(false)
|
||||
|
||||
const {
|
||||
handleImportDSL,
|
||||
handleImportDSLConfirm,
|
||||
versions,
|
||||
isFetching,
|
||||
} = useImportDSL()
|
||||
|
||||
const onConfirmDSL = useCallback(async () => {
|
||||
await handleImportDSLConfirm({
|
||||
onSuccess,
|
||||
})
|
||||
}, [handleImportDSLConfirm, onSuccess])
|
||||
|
||||
const onCreate: CreateAppModalProps['onConfirm'] = async ({
|
||||
name,
|
||||
icon_type,
|
||||
icon,
|
||||
icon_background,
|
||||
description,
|
||||
}) => {
|
||||
hideTryAppPanel()
|
||||
|
||||
const { export_data } = await fetchAppDetail(
|
||||
currApp?.app.id as string,
|
||||
)
|
||||
const payload = {
|
||||
mode: DSLImportMode.YAML_CONTENT,
|
||||
yaml_content: export_data,
|
||||
name,
|
||||
icon_type,
|
||||
icon,
|
||||
icon_background,
|
||||
description,
|
||||
}
|
||||
await handleImportDSL(payload, {
|
||||
onSuccess: () => {
|
||||
setIsShowCreateModal(false)
|
||||
},
|
||||
onPending: () => {
|
||||
setShowDSLConfirmModal(true)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<AppListContext.Provider value={{
|
||||
currentApp: currentTryAppParams,
|
||||
isShowTryAppPanel,
|
||||
setShowTryAppPanel,
|
||||
controlHideCreateFromTemplatePanel,
|
||||
}}
|
||||
>
|
||||
<div className="relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body">
|
||||
<List controlRefreshList={controlRefreshList} />
|
||||
{isShowTryAppPanel && (
|
||||
<TryApp
|
||||
appId={currentTryAppParams?.appId || ''}
|
||||
category={currentTryAppParams?.app?.category}
|
||||
onClose={hideTryAppPanel}
|
||||
onCreate={handleShowFromTryApp}
|
||||
/>
|
||||
)}
|
||||
|
||||
{
|
||||
showDSLConfirmModal && (
|
||||
<DSLConfirmModal
|
||||
versions={versions}
|
||||
onCancel={() => setShowDSLConfirmModal(false)}
|
||||
onConfirm={onConfirmDSL}
|
||||
confirmDisabled={isFetching}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{isShowCreateModal && (
|
||||
<CreateAppModal
|
||||
appIconType={currApp?.app.icon_type || 'emoji'}
|
||||
appIcon={currApp?.app.icon || ''}
|
||||
appIconBackground={currApp?.app.icon_background || ''}
|
||||
appIconUrl={currApp?.app.icon_url}
|
||||
appName={currApp?.app.name || ''}
|
||||
appDescription={currApp?.app.description || ''}
|
||||
show
|
||||
onConfirm={onCreate}
|
||||
confirmDisabled={isFetching}
|
||||
onHide={() => setIsShowCreateModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</AppListContext.Provider>
|
||||
<div className="relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body">
|
||||
<List />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
RiApps2Line,
|
||||
RiDragDropLine,
|
||||
@@ -44,12 +43,7 @@ const CreateFromDSLModal = dynamic(() => import('@/app/components/app/create-fro
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
type Props = {
|
||||
controlRefreshList?: number
|
||||
}
|
||||
const List: FC<Props> = ({
|
||||
controlRefreshList = 0,
|
||||
}) => {
|
||||
const List = () => {
|
||||
const { t } = useTranslation()
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const router = useRouter()
|
||||
@@ -105,12 +99,6 @@ const List: FC<Props> = ({
|
||||
refetch,
|
||||
} = useInfiniteAppList(appListQueryParams, { enabled: !isCurrentWorkspaceDatasetOperator })
|
||||
|
||||
useEffect(() => {
|
||||
if (controlRefreshList > 0)
|
||||
console.log('mute')
|
||||
// mutate()
|
||||
}, [controlRefreshList])
|
||||
|
||||
const anchorRef = useRef<HTMLDivElement>(null)
|
||||
const options = [
|
||||
{ value: 'all', text: t('types.all', { ns: 'app' }), icon: <RiApps2Line className="mr-1 h-[14px] w-[14px]" /> },
|
||||
|
||||
@@ -6,12 +6,10 @@ import {
|
||||
useSearchParams,
|
||||
} from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContextSelector } from 'use-context-selector'
|
||||
import { CreateFromDSLModalTab } from '@/app/components/app/create-from-dsl-modal'
|
||||
import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import AppListContext from '@/context/app-list-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
@@ -57,12 +55,6 @@ const CreateAppCard = ({
|
||||
return undefined
|
||||
}, [dslUrl])
|
||||
|
||||
const controlHideCreateFromTemplatePanel = useContextSelector(AppListContext, ctx => ctx.controlHideCreateFromTemplatePanel)
|
||||
useEffect(() => {
|
||||
if (controlHideCreateFromTemplatePanel > 0)
|
||||
setShowNewAppTemplateDialog(false)
|
||||
}, [controlHideCreateFromTemplatePanel])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
|
||||
@@ -51,15 +51,11 @@ function getActionButtonState(state: ActionButtonState) {
|
||||
}
|
||||
}
|
||||
|
||||
const ActionButton = ({ className, size, state = ActionButtonState.Default, styleCss, children, ref, disabled, ...props }: ActionButtonProps) => {
|
||||
const ActionButton = ({ className, size, state = ActionButtonState.Default, styleCss, children, ref, ...props }: ActionButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
actionButtonVariants({ className, size }),
|
||||
getActionButtonState(state),
|
||||
disabled && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled',
|
||||
)}
|
||||
className={cn(actionButtonVariants({ className, size }), getActionButtonState(state))}
|
||||
ref={ref}
|
||||
style={styleCss}
|
||||
{...props}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiInformation2Fill,
|
||||
} from '@remixicon/react'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import {
|
||||
memo,
|
||||
} from 'react'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
type?: 'info'
|
||||
message: string
|
||||
onHide: () => void
|
||||
className?: string
|
||||
}
|
||||
const bgVariants = cva(
|
||||
'',
|
||||
{
|
||||
variants: {
|
||||
type: {
|
||||
info: 'from-components-badge-status-light-normal-halo to-background-gradient-mask-transparent',
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
const Alert: React.FC<Props> = ({
|
||||
type = 'info',
|
||||
message,
|
||||
onHide,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn('pointer-events-none w-full', className)}>
|
||||
<div
|
||||
className="relative flex space-x-1 overflow-hidden rounded-xl border border-components-panel-border bg-components-panel-bg-blur p-3 shadow-lg"
|
||||
>
|
||||
<div className={cn('pointer-events-none absolute inset-0 bg-gradient-to-r opacity-[0.4]', bgVariants({ type }))}>
|
||||
</div>
|
||||
<div className="flex h-6 w-6 items-center justify-center">
|
||||
<RiInformation2Fill className="text-text-accent" />
|
||||
</div>
|
||||
<div className="p-1">
|
||||
<div className="system-xs-regular text-text-secondary">
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pointer-events-auto flex h-6 w-6 cursor-pointer items-center justify-center"
|
||||
onClick={onHide}
|
||||
>
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Alert)
|
||||
@@ -1,5 +1,5 @@
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { AppSourceType, textToAudioStream } from '@/service/share'
|
||||
import { textToAudioStream } from '@/service/share'
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line ts/consistent-type-definitions
|
||||
@@ -100,7 +100,7 @@ export default class AudioPlayer {
|
||||
|
||||
private async loadAudio() {
|
||||
try {
|
||||
const audioResponse: any = await textToAudioStream(this.url, this.isPublic ? AppSourceType.webApp : AppSourceType.installedApp, { content_type: 'audio/mpeg' }, {
|
||||
const audioResponse: any = await textToAudioStream(this.url, this.isPublic, { content_type: 'audio/mpeg' }, {
|
||||
message_id: this.msgId,
|
||||
streaming: true,
|
||||
voice: this.voice,
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
import type { UseEmblaCarouselType } from 'embla-carousel-react'
|
||||
import Autoplay from 'embla-carousel-autoplay'
|
||||
import useEmblaCarousel from 'embla-carousel-react'
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type CarouselApi = UseEmblaCarouselType[1]
|
||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
||||
type CarouselOptions = UseCarouselParameters[0]
|
||||
type CarouselPlugin = UseCarouselParameters[1]
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: CarouselOptions
|
||||
plugins?: CarouselPlugin
|
||||
orientation?: 'horizontal' | 'vertical'
|
||||
}
|
||||
|
||||
type CarouselContextValue = {
|
||||
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
|
||||
api: ReturnType<typeof useEmblaCarousel>[1]
|
||||
scrollPrev: () => void
|
||||
scrollNext: () => void
|
||||
selectedIndex: number
|
||||
canScrollPrev: boolean
|
||||
canScrollNext: boolean
|
||||
} & CarouselProps
|
||||
|
||||
const CarouselContext = React.createContext<CarouselContextValue | null>(null)
|
||||
|
||||
function useCarousel() {
|
||||
const context = React.useContext(CarouselContext)
|
||||
|
||||
if (!context)
|
||||
throw new Error('useCarousel must be used within a <Carousel />')
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
type TCarousel = {
|
||||
Content: typeof CarouselContent
|
||||
Item: typeof CarouselItem
|
||||
Previous: typeof CarouselPrevious
|
||||
Next: typeof CarouselNext
|
||||
Dot: typeof CarouselDot
|
||||
Plugin: typeof CarouselPlugins
|
||||
} & React.ForwardRefExoticComponent<
|
||||
React.HTMLAttributes<HTMLDivElement> & CarouselProps & React.RefAttributes<CarouselContextValue>
|
||||
>
|
||||
|
||||
const Carousel: TCarousel = React.forwardRef(
|
||||
({ orientation = 'horizontal', opts, plugins, className, children, ...props }, ref) => {
|
||||
const [carouselRef, api] = useEmblaCarousel(
|
||||
{ ...opts, axis: orientation === 'horizontal' ? 'x' : 'y' },
|
||||
plugins,
|
||||
)
|
||||
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
|
||||
const [canScrollNext, setCanScrollNext] = React.useState(false)
|
||||
const [selectedIndex, setSelectedIndex] = React.useState(0)
|
||||
|
||||
const scrollPrev = React.useCallback(() => {
|
||||
api?.scrollPrev()
|
||||
}, [api])
|
||||
|
||||
const scrollNext = React.useCallback(() => {
|
||||
api?.scrollNext()
|
||||
}, [api])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!api)
|
||||
return
|
||||
|
||||
const onSelect = (api: CarouselApi) => {
|
||||
if (!api)
|
||||
return
|
||||
|
||||
setSelectedIndex(api.selectedScrollSnap())
|
||||
setCanScrollPrev(api.canScrollPrev())
|
||||
setCanScrollNext(api.canScrollNext())
|
||||
}
|
||||
|
||||
onSelect(api)
|
||||
api.on('reInit', onSelect)
|
||||
api.on('select', onSelect)
|
||||
|
||||
return () => {
|
||||
api?.off('select', onSelect)
|
||||
}
|
||||
}, [api])
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
carouselRef,
|
||||
api,
|
||||
opts,
|
||||
orientation,
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
selectedIndex,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}))
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider
|
||||
value={{
|
||||
carouselRef,
|
||||
api,
|
||||
opts,
|
||||
orientation,
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
selectedIndex,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={carouselRef}
|
||||
// onKeyDownCapture={handleKeyDown}
|
||||
className={cn('relative overflow-hidden', className)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
)
|
||||
},
|
||||
) as TCarousel
|
||||
Carousel.displayName = 'Carousel'
|
||||
|
||||
const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => {
|
||||
const { orientation } = useCarousel()
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex', orientation === 'vertical' && 'flex-col', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
CarouselContent.displayName = 'CarouselContent'
|
||||
|
||||
const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
className={cn('min-w-0 shrink-0 grow-0 basis-full', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
CarouselItem.displayName = 'CarouselItem'
|
||||
|
||||
type CarouselActionProps = {
|
||||
children?: React.ReactNode
|
||||
} & Omit<React.HTMLAttributes<HTMLButtonElement>, 'disabled' | 'onClick'>
|
||||
|
||||
const CarouselPrevious = React.forwardRef<HTMLButtonElement, CarouselActionProps>(
|
||||
({ children, ...props }, ref) => {
|
||||
const { scrollPrev, canScrollPrev } = useCarousel()
|
||||
|
||||
return (
|
||||
<button ref={ref} {...props} disabled={!canScrollPrev} onClick={scrollPrev}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
)
|
||||
CarouselPrevious.displayName = 'CarouselPrevious'
|
||||
|
||||
const CarouselNext = React.forwardRef<HTMLButtonElement, CarouselActionProps>(
|
||||
({ children, ...props }, ref) => {
|
||||
const { scrollNext, canScrollNext } = useCarousel()
|
||||
|
||||
return (
|
||||
<button ref={ref} {...props} disabled={!canScrollNext} onClick={scrollNext}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
)
|
||||
CarouselNext.displayName = 'CarouselNext'
|
||||
|
||||
const CarouselDot = React.forwardRef<HTMLButtonElement, CarouselActionProps>(
|
||||
({ children, ...props }, ref) => {
|
||||
const { api, selectedIndex } = useCarousel()
|
||||
|
||||
return api?.slideNodes().map((_, index) => {
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
ref={ref}
|
||||
{...props}
|
||||
data-state={index === selectedIndex ? 'active' : 'inactive'}
|
||||
onClick={() => {
|
||||
api.scrollTo(index)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
},
|
||||
)
|
||||
CarouselDot.displayName = 'CarouselDot'
|
||||
|
||||
const CarouselPlugins = {
|
||||
Autoplay,
|
||||
}
|
||||
|
||||
Carousel.Content = CarouselContent
|
||||
Carousel.Item = CarouselItem
|
||||
Carousel.Previous = CarouselPrevious
|
||||
Carousel.Next = CarouselNext
|
||||
Carousel.Dot = CarouselDot
|
||||
Carousel.Plugin = CarouselPlugins
|
||||
|
||||
export { Carousel, useCarousel }
|
||||
@@ -12,7 +12,6 @@ import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import {
|
||||
AppSourceType,
|
||||
fetchSuggestedQuestions,
|
||||
getUrl,
|
||||
stopChatMessageResponding,
|
||||
@@ -53,11 +52,6 @@ const ChatWrapper = () => {
|
||||
initUserVariables,
|
||||
} = useChatWithHistoryContext()
|
||||
|
||||
const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp
|
||||
|
||||
// Semantic variable for better code readability
|
||||
const isHistoryConversation = !!currentConversationId
|
||||
|
||||
const appConfig = useMemo(() => {
|
||||
const config = appParams || {}
|
||||
|
||||
@@ -85,7 +79,7 @@ const ChatWrapper = () => {
|
||||
inputsForm: inputsForms,
|
||||
},
|
||||
appPrevChatTree,
|
||||
taskId => stopChatMessageResponding('', taskId, appSourceType, appId),
|
||||
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
|
||||
clearChatList,
|
||||
setClearChatList,
|
||||
)
|
||||
@@ -144,11 +138,11 @@ const ChatWrapper = () => {
|
||||
}
|
||||
|
||||
handleSend(
|
||||
getUrl('chat-messages', appSourceType, appId || ''),
|
||||
getUrl('chat-messages', isInstalledApp, appId || ''),
|
||||
data,
|
||||
{
|
||||
onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, appSourceType, appId),
|
||||
onConversationComplete: isHistoryConversation ? undefined : handleNewConversationCompleted,
|
||||
onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, isInstalledApp, appId),
|
||||
onConversationComplete: currentConversationId ? undefined : handleNewConversationCompleted,
|
||||
isPublicAPI: !isInstalledApp,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -27,7 +27,6 @@ import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { useAppFavicon } from '@/hooks/use-app-favicon'
|
||||
import { changeLanguage } from '@/i18n-config/client'
|
||||
import {
|
||||
AppSourceType,
|
||||
delConversation,
|
||||
pinConversation,
|
||||
renameConversation,
|
||||
@@ -73,7 +72,6 @@ function getFormattedChatList(messages: any[]) {
|
||||
|
||||
export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
|
||||
const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp
|
||||
const appInfo = useWebAppStore(s => s.appInfo)
|
||||
const appParams = useWebAppStore(s => s.appParams)
|
||||
const appMeta = useWebAppStore(s => s.appMeta)
|
||||
@@ -179,7 +177,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
}, [currentConversationId, newConversationId])
|
||||
|
||||
const { data: appPinnedConversationData } = useShareConversations({
|
||||
appSourceType,
|
||||
isInstalledApp,
|
||||
appId,
|
||||
pinned: true,
|
||||
limit: 100,
|
||||
@@ -192,7 +190,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
data: appConversationData,
|
||||
isLoading: appConversationDataLoading,
|
||||
} = useShareConversations({
|
||||
appSourceType,
|
||||
isInstalledApp,
|
||||
appId,
|
||||
pinned: false,
|
||||
limit: 100,
|
||||
@@ -206,7 +204,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
isLoading: appChatListDataLoading,
|
||||
} = useShareChatList({
|
||||
conversationId: chatShouldReloadKey,
|
||||
appSourceType,
|
||||
isInstalledApp,
|
||||
appId,
|
||||
}, {
|
||||
enabled: !!chatShouldReloadKey,
|
||||
@@ -336,11 +334,10 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
|
||||
const { data: newConversation } = useShareConversationName({
|
||||
conversationId: newConversationId,
|
||||
appSourceType,
|
||||
isInstalledApp,
|
||||
appId,
|
||||
}, {
|
||||
refetchOnWindowFocus: false,
|
||||
enabled: !!newConversationId,
|
||||
})
|
||||
const [originConversationList, setOriginConversationList] = useState<ConversationItem[]>([])
|
||||
useEffect(() => {
|
||||
@@ -465,16 +462,16 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
}, [invalidateShareConversations])
|
||||
|
||||
const handlePinConversation = useCallback(async (conversationId: string) => {
|
||||
await pinConversation(appSourceType, appId, conversationId)
|
||||
await pinConversation(isInstalledApp, appId, conversationId)
|
||||
notify({ type: 'success', message: t('api.success', { ns: 'common' }) })
|
||||
handleUpdateConversationList()
|
||||
}, [appSourceType, appId, notify, t, handleUpdateConversationList])
|
||||
}, [isInstalledApp, appId, notify, t, handleUpdateConversationList])
|
||||
|
||||
const handleUnpinConversation = useCallback(async (conversationId: string) => {
|
||||
await unpinConversation(appSourceType, appId, conversationId)
|
||||
await unpinConversation(isInstalledApp, appId, conversationId)
|
||||
notify({ type: 'success', message: t('api.success', { ns: 'common' }) })
|
||||
handleUpdateConversationList()
|
||||
}, [appSourceType, appId, notify, t, handleUpdateConversationList])
|
||||
}, [isInstalledApp, appId, notify, t, handleUpdateConversationList])
|
||||
|
||||
const [conversationDeleting, setConversationDeleting] = useState(false)
|
||||
const handleDeleteConversation = useCallback(async (
|
||||
@@ -488,7 +485,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
|
||||
try {
|
||||
setConversationDeleting(true)
|
||||
await delConversation(appSourceType, appId, conversationId)
|
||||
await delConversation(isInstalledApp, appId, conversationId)
|
||||
notify({ type: 'success', message: t('api.success', { ns: 'common' }) })
|
||||
onSuccess()
|
||||
}
|
||||
@@ -523,7 +520,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
|
||||
setConversationRenaming(true)
|
||||
try {
|
||||
await renameConversation(appSourceType, appId, conversationId, newName)
|
||||
await renameConversation(isInstalledApp, appId, conversationId, newName)
|
||||
|
||||
notify({
|
||||
type: 'success',
|
||||
@@ -553,9 +550,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
|
||||
}, [handleConversationIdInfoChange, invalidateShareConversations])
|
||||
|
||||
const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => {
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, appSourceType, appId)
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, appId)
|
||||
notify({ type: 'success', message: t('api.success', { ns: 'common' }) })
|
||||
}, [appSourceType, appId, t, notify])
|
||||
}, [isInstalledApp, appId, t, notify])
|
||||
|
||||
return {
|
||||
isInstalledApp,
|
||||
|
||||
@@ -150,7 +150,7 @@ const Answer: FC<AnswerProps> = ({
|
||||
data={workflowProcess}
|
||||
item={item}
|
||||
hideProcessDetail={hideProcessDetail}
|
||||
readonly={hideProcessDetail && appData ? !appData.site?.show_workflow_steps : undefined}
|
||||
readonly={hideProcessDetail && appData ? !appData.site.show_workflow_steps : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { FC } from 'react'
|
||||
import type { ChatItem } from '../../types'
|
||||
import { memo } from 'react'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { useChatContext } from '../context'
|
||||
|
||||
type SuggestedQuestionsProps = {
|
||||
@@ -10,7 +9,7 @@ type SuggestedQuestionsProps = {
|
||||
const SuggestedQuestions: FC<SuggestedQuestionsProps> = ({
|
||||
item,
|
||||
}) => {
|
||||
const { onSend, readonly } = useChatContext()
|
||||
const { onSend } = useChatContext()
|
||||
|
||||
const {
|
||||
isOpeningStatement,
|
||||
@@ -25,11 +24,8 @@ const SuggestedQuestions: FC<SuggestedQuestionsProps> = ({
|
||||
{suggestedQuestions.filter(q => !!q && q.trim()).map((question, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
'system-sm-medium mr-1 mt-1 inline-flex max-w-full shrink-0 cursor-pointer flex-wrap rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-3.5 py-2 text-components-button-secondary-accent-text shadow-xs last:mr-0 hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover',
|
||||
readonly && 'pointer-events-none opacity-50',
|
||||
)}
|
||||
onClick={() => !readonly && onSend?.(question)}
|
||||
className="system-sm-medium mr-1 mt-1 inline-flex max-w-full shrink-0 cursor-pointer flex-wrap rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-3.5 py-2 text-components-button-secondary-accent-text shadow-xs last:mr-0 hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover"
|
||||
onClick={() => onSend?.(question)}
|
||||
>
|
||||
{question}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,6 @@ import type {
|
||||
} from '../../types'
|
||||
import type { InputForm } from '../type'
|
||||
import type { FileUpload } from '@/app/components/base/features/types'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { decode } from 'html-entities'
|
||||
import Recorder from 'js-audio-recorder'
|
||||
import {
|
||||
@@ -31,7 +30,6 @@ import { useTextAreaHeight } from './hooks'
|
||||
import Operation from './operation'
|
||||
|
||||
type ChatInputAreaProps = {
|
||||
readonly?: boolean
|
||||
botName?: string
|
||||
showFeatureBar?: boolean
|
||||
showFileUpload?: boolean
|
||||
@@ -47,7 +45,6 @@ type ChatInputAreaProps = {
|
||||
disabled?: boolean
|
||||
}
|
||||
const ChatInputArea = ({
|
||||
readonly,
|
||||
botName,
|
||||
showFeatureBar,
|
||||
showFileUpload,
|
||||
@@ -173,7 +170,6 @@ const ChatInputArea = ({
|
||||
const operation = (
|
||||
<Operation
|
||||
ref={holdSpaceRef}
|
||||
readonly={readonly}
|
||||
fileConfig={visionConfig}
|
||||
speechToTextConfig={speechToTextConfig}
|
||||
onShowVoiceInput={handleShowVoiceInput}
|
||||
@@ -209,7 +205,7 @@ const ChatInputArea = ({
|
||||
className={cn(
|
||||
'body-lg-regular w-full resize-none bg-transparent p-1 leading-6 text-text-primary outline-none',
|
||||
)}
|
||||
placeholder={decode(t(readonly ? 'chat.inputDisabledPlaceholder' : 'chat.inputPlaceholder', { ns: 'common', botName }) || '')}
|
||||
placeholder={decode(t('chat.inputPlaceholder', { ns: 'common', botName }) || '')}
|
||||
autoFocus
|
||||
minRows={1}
|
||||
value={query}
|
||||
@@ -222,7 +218,6 @@ const ChatInputArea = ({
|
||||
onDragLeave={handleDragFileLeave}
|
||||
onDragOver={handleDragFileOver}
|
||||
onDrop={handleDropFile}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
@@ -244,14 +239,7 @@ const ChatInputArea = ({
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{showFeatureBar && (
|
||||
<FeatureBar
|
||||
showFileUpload={showFileUpload}
|
||||
disabled={featureBarDisabled}
|
||||
onFeatureBarClick={readonly ? noop : onFeatureBarClick}
|
||||
hideEditEntrance={readonly}
|
||||
/>
|
||||
)}
|
||||
{showFeatureBar && <FeatureBar showFileUpload={showFileUpload} disabled={featureBarDisabled} onFeatureBarClick={onFeatureBarClick} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
RiMicLine,
|
||||
RiSendPlane2Fill,
|
||||
} from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { memo } from 'react'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Button from '@/app/components/base/button'
|
||||
@@ -16,7 +15,6 @@ import { FileUploaderInChatInput } from '@/app/components/base/file-uploader'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type OperationProps = {
|
||||
readonly?: boolean
|
||||
fileConfig?: FileUpload
|
||||
speechToTextConfig?: EnableType
|
||||
onShowVoiceInput?: () => void
|
||||
@@ -25,7 +23,6 @@ type OperationProps = {
|
||||
ref?: Ref<HTMLDivElement>
|
||||
}
|
||||
const Operation: FC<OperationProps> = ({
|
||||
readonly,
|
||||
ref,
|
||||
fileConfig,
|
||||
speechToTextConfig,
|
||||
@@ -44,12 +41,11 @@ const Operation: FC<OperationProps> = ({
|
||||
ref={ref}
|
||||
>
|
||||
<div className="flex items-center space-x-1">
|
||||
{fileConfig?.enabled && <FileUploaderInChatInput readonly={readonly} fileConfig={fileConfig} />}
|
||||
{fileConfig?.enabled && <FileUploaderInChatInput fileConfig={fileConfig} />}
|
||||
{
|
||||
speechToTextConfig?.enabled && (
|
||||
<ActionButton
|
||||
size="l"
|
||||
disabled={readonly}
|
||||
onClick={onShowVoiceInput}
|
||||
>
|
||||
<RiMicLine className="h-5 w-5" />
|
||||
@@ -60,7 +56,7 @@ const Operation: FC<OperationProps> = ({
|
||||
<Button
|
||||
className="ml-3 w-8 px-0"
|
||||
variant="primary"
|
||||
onClick={readonly ? noop : onSend}
|
||||
onClick={onSend}
|
||||
style={
|
||||
theme
|
||||
? {
|
||||
|
||||
@@ -15,14 +15,10 @@ export type ChatContextValue = Pick<ChatProps, 'config'
|
||||
| 'onAnnotationEdited'
|
||||
| 'onAnnotationAdded'
|
||||
| 'onAnnotationRemoved'
|
||||
| 'disableFeedback'
|
||||
| 'onFeedback'> & {
|
||||
readonly?: boolean
|
||||
}
|
||||
| 'onFeedback'>
|
||||
|
||||
const ChatContext = createContext<ChatContextValue>({
|
||||
chatList: [],
|
||||
readonly: false,
|
||||
})
|
||||
|
||||
type ChatContextProviderProps = {
|
||||
@@ -31,7 +27,6 @@ type ChatContextProviderProps = {
|
||||
|
||||
export const ChatContextProvider = ({
|
||||
children,
|
||||
readonly = false,
|
||||
config,
|
||||
isResponding,
|
||||
chatList,
|
||||
@@ -43,13 +38,11 @@ export const ChatContextProvider = ({
|
||||
onAnnotationEdited,
|
||||
onAnnotationAdded,
|
||||
onAnnotationRemoved,
|
||||
disableFeedback,
|
||||
onFeedback,
|
||||
}: ChatContextProviderProps) => {
|
||||
return (
|
||||
<ChatContext.Provider value={{
|
||||
config,
|
||||
readonly,
|
||||
isResponding,
|
||||
chatList: chatList || [],
|
||||
showPromptLog,
|
||||
@@ -60,7 +53,6 @@ export const ChatContextProvider = ({
|
||||
onAnnotationEdited,
|
||||
onAnnotationAdded,
|
||||
onAnnotationRemoved,
|
||||
disableFeedback,
|
||||
onFeedback,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -36,8 +36,6 @@ import Question from './question'
|
||||
import TryToAsk from './try-to-ask'
|
||||
|
||||
export type ChatProps = {
|
||||
isTryApp?: boolean
|
||||
readonly?: boolean
|
||||
appData?: AppData
|
||||
chatList: ChatItem[]
|
||||
config?: ChatConfig
|
||||
@@ -62,7 +60,6 @@ export type ChatProps = {
|
||||
onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void
|
||||
onAnnotationRemoved?: (index: number) => void
|
||||
chatNode?: ReactNode
|
||||
disableFeedback?: boolean
|
||||
onFeedback?: (messageId: string, feedback: Feedback) => void
|
||||
chatAnswerContainerInner?: string
|
||||
hideProcessDetail?: boolean
|
||||
@@ -78,8 +75,6 @@ export type ChatProps = {
|
||||
}
|
||||
|
||||
const Chat: FC<ChatProps> = ({
|
||||
isTryApp,
|
||||
readonly = false,
|
||||
appData,
|
||||
config,
|
||||
onSend,
|
||||
@@ -103,7 +98,6 @@ const Chat: FC<ChatProps> = ({
|
||||
onAnnotationEdited,
|
||||
onAnnotationRemoved,
|
||||
chatNode,
|
||||
disableFeedback,
|
||||
onFeedback,
|
||||
chatAnswerContainerInner,
|
||||
hideProcessDetail,
|
||||
@@ -251,7 +245,6 @@ const Chat: FC<ChatProps> = ({
|
||||
|
||||
return (
|
||||
<ChatContextProvider
|
||||
readonly={readonly}
|
||||
config={config}
|
||||
chatList={chatList}
|
||||
isResponding={isResponding}
|
||||
@@ -263,18 +256,17 @@ const Chat: FC<ChatProps> = ({
|
||||
onAnnotationAdded={onAnnotationAdded}
|
||||
onAnnotationEdited={onAnnotationEdited}
|
||||
onAnnotationRemoved={onAnnotationRemoved}
|
||||
disableFeedback={disableFeedback}
|
||||
onFeedback={onFeedback}
|
||||
>
|
||||
<div className={cn('relative h-full', isTryApp && 'flex flex-col')}>
|
||||
<div className="relative h-full">
|
||||
<div
|
||||
ref={chatContainerRef}
|
||||
className={cn('relative h-full overflow-y-auto overflow-x-hidden', isTryApp && 'h-0 grow', chatContainerClassName)}
|
||||
className={cn('relative h-full overflow-y-auto overflow-x-hidden', chatContainerClassName)}
|
||||
>
|
||||
{chatNode}
|
||||
<div
|
||||
ref={chatContainerInnerRef}
|
||||
className={cn('w-full', !noSpacing && 'px-8', chatContainerInnerClassName, isTryApp && 'px-0')}
|
||||
className={cn('w-full', !noSpacing && 'px-8', chatContainerInnerClassName)}
|
||||
>
|
||||
{
|
||||
chatList.map((item, index) => {
|
||||
@@ -318,7 +310,7 @@ const Chat: FC<ChatProps> = ({
|
||||
>
|
||||
<div
|
||||
ref={chatFooterInnerRef}
|
||||
className={cn('relative', chatFooterInnerClassName, isTryApp && 'px-0')}
|
||||
className={cn('relative', chatFooterInnerClassName)}
|
||||
>
|
||||
{
|
||||
!noStopResponding && isResponding && (
|
||||
@@ -341,7 +333,7 @@ const Chat: FC<ChatProps> = ({
|
||||
{
|
||||
!noChatInput && (
|
||||
<ChatInputArea
|
||||
botName={appData?.site?.title || 'Bot'}
|
||||
botName={appData?.site.title || 'Bot'}
|
||||
disabled={inputDisabled}
|
||||
showFeatureBar={showFeatureBar}
|
||||
showFileUpload={showFileUpload}
|
||||
@@ -354,7 +346,6 @@ const Chat: FC<ChatProps> = ({
|
||||
inputsForm={inputsForm}
|
||||
theme={themeBuilder?.theme}
|
||||
isResponding={isResponding}
|
||||
readonly={readonly}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import LogoAvatar from '@/app/components/base/logo/logo-embedded-chat-avatar'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import {
|
||||
AppSourceType,
|
||||
fetchSuggestedQuestions,
|
||||
getUrl,
|
||||
stopChatMessageResponding,
|
||||
@@ -43,7 +42,6 @@ const ChatWrapper = () => {
|
||||
isInstalledApp,
|
||||
appId,
|
||||
appMeta,
|
||||
disableFeedback,
|
||||
handleFeedback,
|
||||
currentChatInstanceRef,
|
||||
themeBuilder,
|
||||
@@ -52,9 +50,7 @@ const ChatWrapper = () => {
|
||||
setIsResponding,
|
||||
allInputsHidden,
|
||||
initUserVariables,
|
||||
appSourceType,
|
||||
} = useEmbeddedChatbotContext()
|
||||
|
||||
const appConfig = useMemo(() => {
|
||||
const config = appParams || {}
|
||||
|
||||
@@ -82,7 +78,7 @@ const ChatWrapper = () => {
|
||||
inputsForm: inputsForms,
|
||||
},
|
||||
appPrevChatList,
|
||||
taskId => stopChatMessageResponding('', taskId, appSourceType, appId),
|
||||
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
|
||||
clearChatList,
|
||||
setClearChatList,
|
||||
)
|
||||
@@ -138,13 +134,14 @@ const ChatWrapper = () => {
|
||||
conversation_id: currentConversationId,
|
||||
parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null,
|
||||
}
|
||||
|
||||
handleSend(
|
||||
getUrl('chat-messages', appSourceType, appId || ''),
|
||||
getUrl('chat-messages', isInstalledApp, appId || ''),
|
||||
data,
|
||||
{
|
||||
onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, appSourceType, appId),
|
||||
onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, isInstalledApp, appId),
|
||||
onConversationComplete: currentConversationId ? undefined : handleNewConversationCompleted,
|
||||
isPublicAPI: appSourceType === AppSourceType.webApp,
|
||||
isPublicAPI: !isInstalledApp,
|
||||
},
|
||||
)
|
||||
}, [currentConversationId, currentConversationInputs, newConversationInputs, chatList, handleSend, isInstalledApp, appId, handleNewConversationCompleted])
|
||||
@@ -162,8 +159,7 @@ const ChatWrapper = () => {
|
||||
return chatList.filter(item => !item.isOpeningStatement)
|
||||
}, [chatList, currentConversationId])
|
||||
|
||||
const isTryApp = appSourceType === AppSourceType.tryApp
|
||||
const [collapsed, setCollapsed] = useState(!!currentConversationId && !isTryApp) // try app always use the new chat
|
||||
const [collapsed, setCollapsed] = useState(!!currentConversationId)
|
||||
|
||||
const chatNode = useMemo(() => {
|
||||
if (allInputsHidden || !inputsForms.length)
|
||||
@@ -188,8 +184,6 @@ const ChatWrapper = () => {
|
||||
return null
|
||||
if (!collapsed && inputsForms.length > 0 && !allInputsHidden)
|
||||
return null
|
||||
if (!appData?.site)
|
||||
return null
|
||||
if (welcomeMessage.suggestedQuestions && welcomeMessage.suggestedQuestions?.length > 0) {
|
||||
return (
|
||||
<div className={cn('flex items-center justify-center px-4 py-12', isMobile ? 'min-h-[30vh] py-0' : 'h-[50vh]')}>
|
||||
@@ -223,7 +217,7 @@ const ChatWrapper = () => {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}, [appData?.site, chatList, collapsed, currentConversationId, inputsForms.length, respondingState, allInputsHidden])
|
||||
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length, respondingState, allInputsHidden])
|
||||
|
||||
const answerIcon = isDify()
|
||||
? <LogoAvatar className="relative shrink-0" />
|
||||
@@ -240,7 +234,6 @@ const ChatWrapper = () => {
|
||||
|
||||
return (
|
||||
<Chat
|
||||
isTryApp={isTryApp}
|
||||
appData={appData || undefined}
|
||||
config={appConfig}
|
||||
chatList={messageList}
|
||||
@@ -260,7 +253,6 @@ const ChatWrapper = () => {
|
||||
</>
|
||||
)}
|
||||
allToolIcons={appMeta?.tool_icons || {}}
|
||||
disableFeedback={disableFeedback}
|
||||
onFeedback={handleFeedback}
|
||||
suggestedQuestions={suggestedQuestions}
|
||||
answerIcon={answerIcon}
|
||||
|
||||
@@ -15,7 +15,6 @@ import type {
|
||||
} from '@/models/share'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { createContext, useContext } from 'use-context-selector'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
|
||||
export type EmbeddedChatbotContextValue = {
|
||||
appMeta: AppMeta | null
|
||||
@@ -38,10 +37,8 @@ export type EmbeddedChatbotContextValue = {
|
||||
chatShouldReloadKey: string
|
||||
isMobile: boolean
|
||||
isInstalledApp: boolean
|
||||
appSourceType: AppSourceType
|
||||
allowResetChat: boolean
|
||||
appId?: string
|
||||
disableFeedback?: boolean
|
||||
handleFeedback: (messageId: string, feedback: Feedback) => void
|
||||
currentChatInstanceRef: RefObject<{ handleStop: () => void }>
|
||||
themeBuilder?: ThemeBuilder
|
||||
@@ -77,7 +74,6 @@ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>
|
||||
handleNewConversationCompleted: noop,
|
||||
chatShouldReloadKey: '',
|
||||
isMobile: false,
|
||||
appSourceType: AppSourceType.webApp,
|
||||
isInstalledApp: false,
|
||||
allowResetChat: true,
|
||||
handleFeedback: noop,
|
||||
|
||||
@@ -5,7 +5,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { ToastProvider } from '@/app/components/base/toast'
|
||||
import {
|
||||
AppSourceType,
|
||||
fetchChatList,
|
||||
fetchConversations,
|
||||
generationConversationName,
|
||||
@@ -146,7 +145,7 @@ describe('useEmbeddedChatbot', () => {
|
||||
mockFetchChatList.mockResolvedValue({ data: [] })
|
||||
|
||||
// Act
|
||||
const { result } = renderWithClient(() => useEmbeddedChatbot(AppSourceType.webApp))
|
||||
const { result } = renderWithClient(() => useEmbeddedChatbot())
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
@@ -178,7 +177,7 @@ describe('useEmbeddedChatbot', () => {
|
||||
mockFetchChatList.mockResolvedValue({ data: [] })
|
||||
mockGenerationConversationName.mockResolvedValue(generatedConversation)
|
||||
|
||||
const { result, queryClient } = renderWithClient(() => useEmbeddedChatbot(AppSourceType.webApp))
|
||||
const { result, queryClient } = renderWithClient(() => useEmbeddedChatbot())
|
||||
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries')
|
||||
|
||||
// Act
|
||||
@@ -208,7 +207,7 @@ describe('useEmbeddedChatbot', () => {
|
||||
mockFetchChatList.mockResolvedValue({ data: [] })
|
||||
mockGenerationConversationName.mockResolvedValue(createConversationItem({ id: 'conversation-1' }))
|
||||
|
||||
const { result } = renderWithClient(() => useEmbeddedChatbot(AppSourceType.webApp))
|
||||
const { result } = renderWithClient(() => useEmbeddedChatbot())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchChatList).toHaveBeenCalledTimes(1)
|
||||
@@ -238,7 +237,7 @@ describe('useEmbeddedChatbot', () => {
|
||||
mockFetchChatList.mockResolvedValue({ data: [] })
|
||||
mockGenerationConversationName.mockResolvedValue(createConversationItem({ id: 'conversation-new' }))
|
||||
|
||||
const { result } = renderWithClient(() => useEmbeddedChatbot(AppSourceType.webApp))
|
||||
const { result } = renderWithClient(() => useEmbeddedChatbot())
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
} from '../types'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import type {
|
||||
AppData,
|
||||
// AppData,
|
||||
ConversationItem,
|
||||
} from '@/models/share'
|
||||
import { useLocalStorageState } from 'ahooks'
|
||||
@@ -24,14 +24,13 @@ import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import { changeLanguage } from '@/i18n-config/client'
|
||||
import { AppSourceType, updateFeedback } from '@/service/share'
|
||||
import { updateFeedback } from '@/service/share'
|
||||
import {
|
||||
useInvalidateShareConversations,
|
||||
useShareChatList,
|
||||
useShareConversationName,
|
||||
useShareConversations,
|
||||
} from '@/service/use-share'
|
||||
import { useGetTryAppInfo, useGetTryAppParams } from '@/service/use-try-app'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { getProcessedFilesFromResponse } from '../../file-uploader/utils'
|
||||
import { CONVERSATION_ID_INFO } from '../constants'
|
||||
@@ -63,36 +62,18 @@ function getFormattedChatList(messages: any[]) {
|
||||
return newChatList
|
||||
}
|
||||
|
||||
export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: string) => {
|
||||
const isInstalledApp = false // just can be webapp and try app
|
||||
const isTryApp = appSourceType === AppSourceType.tryApp
|
||||
const { data: tryAppInfo } = useGetTryAppInfo(isTryApp ? tryAppId! : '')
|
||||
const webAppInfo = useWebAppStore(s => s.appInfo)
|
||||
const appInfo = isTryApp ? tryAppInfo : webAppInfo
|
||||
export const useEmbeddedChatbot = () => {
|
||||
const isInstalledApp = false
|
||||
const appInfo = useWebAppStore(s => s.appInfo)
|
||||
const appMeta = useWebAppStore(s => s.appMeta)
|
||||
const { data: tryAppParams } = useGetTryAppParams(isTryApp ? tryAppId! : '')
|
||||
const webAppParams = useWebAppStore(s => s.appParams)
|
||||
const appParams = isTryApp ? tryAppParams : webAppParams
|
||||
|
||||
const appId = useMemo(() => {
|
||||
return isTryApp ? tryAppId : (appInfo as any)?.app_id
|
||||
}, [appInfo])
|
||||
|
||||
const appParams = useWebAppStore(s => s.appParams)
|
||||
const embeddedConversationId = useWebAppStore(s => s.embeddedConversationId)
|
||||
const embeddedUserId = useWebAppStore(s => s.embeddedUserId)
|
||||
const appId = useMemo(() => appInfo?.app_id, [appInfo])
|
||||
|
||||
const [userId, setUserId] = useState<string>()
|
||||
const [conversationId, setConversationId] = useState<string>()
|
||||
|
||||
useEffect(() => {
|
||||
if (isTryApp)
|
||||
return
|
||||
getProcessedSystemVariablesFromUrlParams().then(({ user_id, conversation_id }) => {
|
||||
setUserId(user_id)
|
||||
setConversationId(conversation_id)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setUserId(embeddedUserId || undefined)
|
||||
}, [embeddedUserId])
|
||||
@@ -102,8 +83,6 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
}, [embeddedConversationId])
|
||||
|
||||
useEffect(() => {
|
||||
if (isTryApp)
|
||||
return
|
||||
const setLanguageFromParams = async () => {
|
||||
// Check URL parameters for language override
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
@@ -121,9 +100,9 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
// If locale is set as a system variable, use that
|
||||
await changeLanguage(localeFromSysVar)
|
||||
}
|
||||
else if ((appInfo as unknown as AppData)?.site?.default_language) {
|
||||
else if (appInfo?.site.default_language) {
|
||||
// Otherwise use the default from app config
|
||||
await changeLanguage((appInfo as unknown as AppData).site?.default_language)
|
||||
await changeLanguage(appInfo.site.default_language)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,13 +112,6 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<Record<string, Record<string, string>>>(CONVERSATION_ID_INFO, {
|
||||
defaultValue: {},
|
||||
})
|
||||
const removeConversationIdInfo = useCallback((appId: string) => {
|
||||
setConversationIdInfo((prev) => {
|
||||
const newInfo = { ...prev }
|
||||
delete newInfo[appId]
|
||||
return newInfo
|
||||
})
|
||||
}, [setConversationIdInfo])
|
||||
const allowResetChat = !conversationId
|
||||
const currentConversationId = useMemo(() => conversationIdInfo?.[appId || '']?.[userId || 'DEFAULT'] || conversationId || '', [appId, conversationIdInfo, userId, conversationId])
|
||||
const handleConversationIdInfoChange = useCallback((changeConversationId: string) => {
|
||||
@@ -166,7 +138,7 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
}, [currentConversationId, newConversationId])
|
||||
|
||||
const { data: appPinnedConversationData } = useShareConversations({
|
||||
appSourceType,
|
||||
isInstalledApp,
|
||||
appId,
|
||||
pinned: true,
|
||||
limit: 100,
|
||||
@@ -175,7 +147,7 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
data: appConversationData,
|
||||
isLoading: appConversationDataLoading,
|
||||
} = useShareConversations({
|
||||
appSourceType,
|
||||
isInstalledApp,
|
||||
appId,
|
||||
pinned: false,
|
||||
limit: 100,
|
||||
@@ -185,7 +157,7 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
isLoading: appChatListDataLoading,
|
||||
} = useShareChatList({
|
||||
conversationId: chatShouldReloadKey,
|
||||
appSourceType,
|
||||
isInstalledApp,
|
||||
appId,
|
||||
})
|
||||
const invalidateShareConversations = useInvalidateShareConversations()
|
||||
@@ -293,8 +265,6 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
useEffect(() => {
|
||||
// init inputs from url params
|
||||
(async () => {
|
||||
if (isTryApp)
|
||||
return
|
||||
const inputs = await getProcessedInputsFromUrlParams()
|
||||
const userVariables = await getProcessedUserVariablesFromUrlParams()
|
||||
setInitInputs(inputs)
|
||||
@@ -312,11 +282,10 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
|
||||
const { data: newConversation } = useShareConversationName({
|
||||
conversationId: newConversationId,
|
||||
appSourceType,
|
||||
isInstalledApp,
|
||||
appId,
|
||||
}, {
|
||||
refetchOnWindowFocus: false,
|
||||
enabled: !isTryApp,
|
||||
})
|
||||
const [originConversationList, setOriginConversationList] = useState<ConversationItem[]>([])
|
||||
useEffect(() => {
|
||||
@@ -366,7 +335,7 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
}, [appChatListData, currentConversationId])
|
||||
const [currentConversationInputs, setCurrentConversationInputs] = useState<Record<string, any>>(currentConversationLatestInputs || {})
|
||||
useEffect(() => {
|
||||
if (currentConversationItem && !isTryApp)
|
||||
if (currentConversationItem)
|
||||
setCurrentConversationInputs(currentConversationLatestInputs || {})
|
||||
}, [currentConversationItem, currentConversationLatestInputs])
|
||||
|
||||
@@ -426,17 +395,12 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
setClearChatList(false)
|
||||
}, [handleConversationIdInfoChange, setClearChatList])
|
||||
const handleNewConversation = useCallback(async () => {
|
||||
if (isTryApp) {
|
||||
setClearChatList(true)
|
||||
return
|
||||
}
|
||||
|
||||
currentChatInstanceRef.current.handleStop()
|
||||
setShowNewConversationItemInList(true)
|
||||
handleChangeConversation('')
|
||||
handleNewConversationInputsChange(await getProcessedInputsFromUrlParams())
|
||||
setClearChatList(true)
|
||||
}, [isTryApp, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList])
|
||||
}, [handleChangeConversation, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList])
|
||||
|
||||
const handleNewConversationCompleted = useCallback((newConversationId: string) => {
|
||||
setNewConversationId(newConversationId)
|
||||
@@ -446,18 +410,16 @@ export const useEmbeddedChatbot = (appSourceType: AppSourceType, tryAppId?: stri
|
||||
}, [handleConversationIdInfoChange, invalidateShareConversations])
|
||||
|
||||
const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => {
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, appSourceType, appId)
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, appId)
|
||||
notify({ type: 'success', message: t('api.success', { ns: 'common' }) })
|
||||
}, [appSourceType, appId, t, notify])
|
||||
}, [isInstalledApp, appId, t, notify])
|
||||
|
||||
return {
|
||||
appSourceType,
|
||||
isInstalledApp,
|
||||
allowResetChat,
|
||||
appId,
|
||||
currentConversationId,
|
||||
currentConversationItem,
|
||||
removeConversationIdInfo,
|
||||
handleConversationIdInfoChange,
|
||||
appData: appInfo,
|
||||
appParams: appParams || {} as ChatConfig,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
'use client'
|
||||
import type { AppData } from '@/models/share'
|
||||
import {
|
||||
useEffect,
|
||||
} from 'react'
|
||||
@@ -12,7 +11,6 @@ import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import {
|
||||
EmbeddedChatbotContext,
|
||||
@@ -134,12 +132,11 @@ const EmbeddedChatbotWrapper = () => {
|
||||
setCurrentConversationInputs,
|
||||
allInputsHidden,
|
||||
initUserVariables,
|
||||
} = useEmbeddedChatbot(AppSourceType.webApp)
|
||||
} = useEmbeddedChatbot()
|
||||
|
||||
return (
|
||||
<EmbeddedChatbotContext.Provider value={{
|
||||
appSourceType: AppSourceType.webApp,
|
||||
appData: (appData as AppData) || null,
|
||||
appData,
|
||||
appParams,
|
||||
appMeta,
|
||||
appChatListDataLoading,
|
||||
|
||||
@@ -4,7 +4,6 @@ import Button from '@/app/components/base/button'
|
||||
import InputsFormContent from '@/app/components/base/chat/embedded-chatbot/inputs-form/content'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { Message3Fill } from '@/app/components/base/icons/src/public/other'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { useEmbeddedChatbotContext } from '../context'
|
||||
|
||||
@@ -19,7 +18,6 @@ const InputsFormNode = ({
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
appSourceType,
|
||||
isMobile,
|
||||
currentConversationId,
|
||||
themeBuilder,
|
||||
@@ -27,17 +25,15 @@ const InputsFormNode = ({
|
||||
allInputsHidden,
|
||||
inputsForms,
|
||||
} = useEmbeddedChatbotContext()
|
||||
const isTryApp = appSourceType === AppSourceType.tryApp
|
||||
|
||||
if (allInputsHidden || inputsForms.length === 0)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className={cn('mb-6 flex flex-col items-center px-4 pt-6', isMobile && 'mb-4 pt-4', isTryApp && 'mb-0 px-0')}>
|
||||
<div className={cn('mb-6 flex flex-col items-center px-4 pt-6', isMobile && 'mb-4 pt-4')}>
|
||||
<div className={cn(
|
||||
'w-full max-w-[672px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-md',
|
||||
collapsed && 'border border-components-card-border bg-components-card-bg shadow-none',
|
||||
isTryApp && 'max-w-[auto]',
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
|
||||
@@ -33,7 +33,7 @@ const ViewFormDropdown = ({ iconColor }: Props) => {
|
||||
<RiChatSettingsLine className={cn('h-[18px] w-[18px]', iconColor)} />
|
||||
</ActionButton>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-[99]">
|
||||
<PortalToFollowElemContent className="z-50">
|
||||
<div className="w-[400px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg backdrop-blur-sm">
|
||||
<div className="flex items-center gap-3 rounded-t-2xl border-b border-divider-subtle px-6 py-4">
|
||||
<Message3Fill className="h-6 w-6 shrink-0" />
|
||||
|
||||
@@ -14,7 +14,6 @@ type Props = {
|
||||
showFileUpload?: boolean
|
||||
disabled?: boolean
|
||||
onFeatureBarClick?: (state: boolean) => void
|
||||
hideEditEntrance?: boolean
|
||||
}
|
||||
|
||||
const FeatureBar = ({
|
||||
@@ -22,7 +21,6 @@ const FeatureBar = ({
|
||||
showFileUpload = true,
|
||||
disabled,
|
||||
onFeatureBarClick,
|
||||
hideEditEntrance = false,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const features = useFeatures(s => s.features)
|
||||
@@ -135,14 +133,10 @@ const FeatureBar = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="body-xs-regular grow text-text-tertiary">{t('feature.bar.enableText', { ns: 'appDebug' })}</div>
|
||||
{
|
||||
!hideEditEntrance && (
|
||||
<Button className="shrink-0" variant="ghost-accent" size="small" onClick={() => onFeatureBarClick?.(true)}>
|
||||
<div className="mx-1">{t('feature.bar.manage', { ns: 'appDebug' })}</div>
|
||||
<RiArrowRightLine className="h-3.5 w-3.5 text-text-accent" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<Button className="shrink-0" variant="ghost-accent" size="small" onClick={() => onFeatureBarClick?.(true)}>
|
||||
<div className="mx-1">{t('feature.bar.manage', { ns: 'appDebug' })}</div>
|
||||
<RiArrowRightLine className="h-3.5 w-3.5 text-text-accent" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
import type { OnFeaturesChange } from '@/app/components/base/features/types'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import type { I18nKeysWithPrefix } from '@/types/i18n'
|
||||
import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from '@headlessui/react'
|
||||
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
@@ -19,6 +20,8 @@ import { useAppVoices } from '@/service/use-apps'
|
||||
import { TtsAutoPlay } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type VoiceLanguageKey = I18nKeysWithPrefix<'common', 'voice.language.'>
|
||||
|
||||
type VoiceParamConfigProps = {
|
||||
onClose: () => void
|
||||
onChange?: OnFeaturesChange
|
||||
|
||||
@@ -13,27 +13,21 @@ import FileFromLinkOrLocal from '../file-from-link-or-local'
|
||||
|
||||
type FileUploaderInChatInputProps = {
|
||||
fileConfig: FileUpload
|
||||
readonly?: boolean
|
||||
}
|
||||
const FileUploaderInChatInput = ({
|
||||
fileConfig,
|
||||
readonly,
|
||||
}: FileUploaderInChatInputProps) => {
|
||||
const renderTrigger = useCallback((open: boolean) => {
|
||||
return (
|
||||
<ActionButton
|
||||
size="l"
|
||||
className={cn(open && 'bg-state-base-hover')}
|
||||
disabled={readonly}
|
||||
>
|
||||
<RiAttachmentLine className="h-5 w-5" />
|
||||
</ActionButton>
|
||||
)
|
||||
}, [])
|
||||
|
||||
if (readonly)
|
||||
return renderTrigger(false)
|
||||
|
||||
return (
|
||||
<FileFromLinkOrLocal
|
||||
trigger={renderTrigger}
|
||||
|
||||
@@ -70,12 +70,10 @@ const PasteImageLinkButton: FC<PasteImageLinkButtonProps> = ({
|
||||
type TextGenerationImageUploaderProps = {
|
||||
settings: VisionSettings
|
||||
onFilesChange: (files: ImageFile[]) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
const TextGenerationImageUploader: FC<TextGenerationImageUploaderProps> = ({
|
||||
settings,
|
||||
onFilesChange,
|
||||
disabled,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -95,7 +93,7 @@ const TextGenerationImageUploader: FC<TextGenerationImageUploaderProps> = ({
|
||||
const localUpload = (
|
||||
<Uploader
|
||||
onUpload={onUpload}
|
||||
disabled={files.length >= settings.number_limits || disabled}
|
||||
disabled={files.length >= settings.number_limits}
|
||||
limit={+settings.image_file_size_limit!}
|
||||
>
|
||||
{
|
||||
@@ -117,7 +115,7 @@ const TextGenerationImageUploader: FC<TextGenerationImageUploaderProps> = ({
|
||||
const urlUpload = (
|
||||
<PasteImageLinkButton
|
||||
onUpload={onUpload}
|
||||
disabled={files.length >= settings.number_limits || disabled}
|
||||
disabled={files.length >= settings.number_limits}
|
||||
/>
|
||||
)
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ export type ITabHeaderProps = {
|
||||
items: Item[]
|
||||
value: string
|
||||
itemClassName?: string
|
||||
itemWrapClassName?: string
|
||||
activeItemClassName?: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
@@ -25,8 +23,6 @@ const TabHeader: FC<ITabHeaderProps> = ({
|
||||
items,
|
||||
value,
|
||||
itemClassName,
|
||||
itemWrapClassName,
|
||||
activeItemClassName,
|
||||
onChange,
|
||||
}) => {
|
||||
const renderItem = ({ id, name, icon, extra, disabled }: Item) => (
|
||||
@@ -34,9 +30,8 @@ const TabHeader: FC<ITabHeaderProps> = ({
|
||||
key={id}
|
||||
className={cn(
|
||||
'system-md-semibold relative flex cursor-pointer items-center border-b-2 border-transparent pb-2 pt-2.5',
|
||||
id === value ? cn('border-components-tab-active text-text-primary', activeItemClassName) : 'text-text-tertiary',
|
||||
id === value ? 'border-components-tab-active text-text-primary' : 'text-text-tertiary',
|
||||
disabled && 'cursor-not-allowed opacity-30',
|
||||
itemWrapClassName,
|
||||
)}
|
||||
onClick={() => !disabled && onChange(id)}
|
||||
>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useParams, usePathname } from 'next/navigation'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||
import { AppSourceType, audioToText } from '@/service/share'
|
||||
import { audioToText } from '@/service/share'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import s from './index.module.css'
|
||||
import { convertToMp3 } from './utils'
|
||||
@@ -108,7 +108,7 @@ const VoiceInput = ({
|
||||
}
|
||||
|
||||
try {
|
||||
const audioResponse = await audioToText(url, isPublic ? AppSourceType.webApp : AppSourceType.installedApp, formData)
|
||||
const audioResponse = await audioToText(url, isPublic, formData)
|
||||
onConverted(audioResponse.text)
|
||||
onCancel()
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ vi.mock('../../app/type-selector', () => ({
|
||||
}))
|
||||
|
||||
const createApp = (overrides?: Partial<App>): App => ({
|
||||
can_trial: true,
|
||||
app_id: 'app-id',
|
||||
description: 'App description',
|
||||
copyright: '2024',
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
'use client'
|
||||
import type { App } from '@/models/explore'
|
||||
import { PlusIcon } from '@heroicons/react/20/solid'
|
||||
import { RiInformation2Line } from '@remixicon/react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContextSelector } from 'use-context-selector'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { AppTypeIcon } from '../../app/type-selector'
|
||||
@@ -28,17 +23,8 @@ const AppCard = ({
|
||||
}: AppCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { app: appBasicInfo } = app
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const isTrialApp = app.can_trial && systemFeatures.enable_trial_app
|
||||
const setShowTryAppPanel = useContextSelector(ExploreContext, ctx => ctx.setShowTryAppPanel)
|
||||
const showTryAPPPanel = useCallback((appId: string) => {
|
||||
return () => {
|
||||
setShowTryAppPanel?.(true, { appId, app })
|
||||
}
|
||||
}, [setShowTryAppPanel, app.category])
|
||||
|
||||
return (
|
||||
<div className={cn('group relative col-span-1 flex cursor-pointer flex-col overflow-hidden rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg pb-2 shadow-sm transition-all duration-200 ease-in-out hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-lg')}>
|
||||
<div className={cn('group relative col-span-1 flex cursor-pointer flex-col overflow-hidden rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg pb-2 shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg')}>
|
||||
<div className="flex h-[66px] shrink-0 grow-0 items-center gap-3 px-[14px] pb-3 pt-[14px]">
|
||||
<div className="relative shrink-0">
|
||||
<AppIcon
|
||||
@@ -72,19 +58,13 @@ const AppCard = ({
|
||||
{app.description}
|
||||
</div>
|
||||
</div>
|
||||
{isExplore && (canCreate || isTrialApp) && (
|
||||
{isExplore && canCreate && (
|
||||
<div className={cn('absolute bottom-0 left-0 right-0 hidden bg-gradient-to-t from-components-panel-gradient-2 from-[60.27%] to-transparent p-4 pt-8 group-hover:flex')}>
|
||||
<div className={cn('flex h-8 w-full items-center space-x-2')}>
|
||||
<Button variant="primary" className="h-7 grow" onClick={() => onCreate()}>
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
<span className="text-xs">{t('appCard.addToWorkspace', { ns: 'explore' })}</span>
|
||||
</Button>
|
||||
{isTrialApp && (
|
||||
<Button className="w-full" onClick={showTryAPPPanel(app.app_id)}>
|
||||
<RiInformation2Line className="mr-1 size-4" />
|
||||
<span>{t('appCard.try', { ns: 'explore' })}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -102,7 +102,6 @@ const createApp = (overrides: Partial<App> = {}): App => ({
|
||||
description: overrides.app?.description ?? 'Alpha description',
|
||||
use_icon_as_answer_icon: overrides.app?.use_icon_as_answer_icon ?? false,
|
||||
},
|
||||
can_trial: true,
|
||||
app_id: overrides.app_id ?? 'app-1',
|
||||
description: overrides.description ?? 'Alpha description',
|
||||
copyright: overrides.copyright ?? '',
|
||||
@@ -128,8 +127,6 @@ const renderWithContext = (hasEditPermission = false, onSuccess?: () => void) =>
|
||||
setInstalledApps: vi.fn(),
|
||||
isFetchingInstalledApps: false,
|
||||
setIsFetchingInstalledApps: vi.fn(),
|
||||
isShowTryAppPanel: false,
|
||||
setShowTryAppPanel: vi.fn(),
|
||||
}}
|
||||
>
|
||||
<AppList onSuccess={onSuccess} />
|
||||
|
||||
@@ -7,17 +7,14 @@ import { useQueryState } from 'nuqs'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext, useContextSelector } from 'use-context-selector'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import DSLConfirmModal from '@/app/components/app/create-from-dsl-modal/dsl-confirm-modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import AppCard from '@/app/components/explore/app-card'
|
||||
import Banner from '@/app/components/explore/banner/banner'
|
||||
import Category from '@/app/components/explore/category'
|
||||
import CreateAppModal from '@/app/components/explore/create-app-modal'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useImportDSL } from '@/hooks/use-import-dsl'
|
||||
import {
|
||||
DSLImportMode,
|
||||
@@ -25,7 +22,6 @@ import {
|
||||
import { fetchAppDetail } from '@/service/explore'
|
||||
import { useExploreAppList } from '@/service/use-explore'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import TryApp from '../try-app'
|
||||
import s from './style.module.css'
|
||||
|
||||
type AppsProps = {
|
||||
@@ -36,19 +32,12 @@ const Apps = ({
|
||||
onSuccess,
|
||||
}: AppsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const { hasEditPermission } = useContext(ExploreContext)
|
||||
const allCategoriesEn = t('apps.allCategories', { ns: 'explore', lng: 'en' })
|
||||
|
||||
const [keywords, setKeywords] = useState('')
|
||||
const [searchKeywords, setSearchKeywords] = useState('')
|
||||
|
||||
const hasFilterCondition = !!keywords
|
||||
const handleResetFilter = useCallback(() => {
|
||||
setKeywords('')
|
||||
setSearchKeywords('')
|
||||
}, [])
|
||||
|
||||
const { run: handleSearch } = useDebounceFn(() => {
|
||||
setSearchKeywords(keywords)
|
||||
}, { wait: 500 })
|
||||
@@ -95,18 +84,6 @@ const Apps = ({
|
||||
isFetching,
|
||||
} = useImportDSL()
|
||||
const [showDSLConfirmModal, setShowDSLConfirmModal] = useState(false)
|
||||
|
||||
const isShowTryAppPanel = useContextSelector(ExploreContext, ctx => ctx.isShowTryAppPanel)
|
||||
const setShowTryAppPanel = useContextSelector(ExploreContext, ctx => ctx.setShowTryAppPanel)
|
||||
const hideTryAppPanel = useCallback(() => {
|
||||
setShowTryAppPanel(false)
|
||||
}, [setShowTryAppPanel])
|
||||
const appParams = useContextSelector(ExploreContext, ctx => ctx.currentApp)
|
||||
const handleShowFromTryApp = useCallback(() => {
|
||||
setCurrApp(appParams?.app || null)
|
||||
setIsShowCreateModal(true)
|
||||
}, [appParams?.app])
|
||||
|
||||
const onCreate: CreateAppModalProps['onConfirm'] = async ({
|
||||
name,
|
||||
icon_type,
|
||||
@@ -114,8 +91,6 @@ const Apps = ({
|
||||
icon_background,
|
||||
description,
|
||||
}) => {
|
||||
hideTryAppPanel()
|
||||
|
||||
const { export_data } = await fetchAppDetail(
|
||||
currApp?.app.id as string,
|
||||
)
|
||||
@@ -162,24 +137,22 @@ const Apps = ({
|
||||
'flex h-full flex-col border-l-[0.5px] border-divider-regular',
|
||||
)}
|
||||
>
|
||||
{systemFeatures.enable_explore_banner && (
|
||||
<div className="mt-4 px-12">
|
||||
<Banner />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="shrink-0 px-12 pt-6">
|
||||
<div className={`mb-1 ${s.textGradient} text-xl font-semibold`}>{t('apps.title', { ns: 'explore' })}</div>
|
||||
<div className="text-sm text-text-tertiary">{t('apps.description', { ns: 'explore' })}</div>
|
||||
</div>
|
||||
|
||||
<div className={cn(
|
||||
'mt-6 flex items-center justify-between px-12',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="system-xl-semibold grow truncate text-text-primary">{!hasFilterCondition ? t('apps.title', { ns: 'explore' }) : t('apps.resultNum', { num: searchFilteredList.length, ns: 'explore' })}</div>
|
||||
{hasFilterCondition && (
|
||||
<>
|
||||
<div className="mx-3 h-4 w-px bg-divider-regular"></div>
|
||||
<Button size="medium" onClick={handleResetFilter}>{t('apps.resetFilter', { ns: 'explore' })}</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Category
|
||||
list={categories}
|
||||
value={currCategory}
|
||||
onChange={setCurrCategory}
|
||||
allCategoriesEn={allCategoriesEn}
|
||||
/>
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
@@ -190,15 +163,6 @@ const Apps = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 px-12">
|
||||
<Category
|
||||
list={categories}
|
||||
value={currCategory}
|
||||
onChange={setCurrCategory}
|
||||
allCategoriesEn={allCategoriesEn}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={cn(
|
||||
'relative mt-4 flex flex-1 shrink-0 grow flex-col overflow-auto pb-6',
|
||||
)}
|
||||
@@ -247,15 +211,6 @@ const Apps = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{isShowTryAppPanel && (
|
||||
<TryApp
|
||||
appId={appParams?.appId || ''}
|
||||
category={appParams?.app?.category}
|
||||
onClose={hideTryAppPanel}
|
||||
onCreate={handleShowFromTryApp}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
import type { FC } from 'react'
|
||||
import { RiArrowRightLine } from '@remixicon/react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCarousel } from '@/app/components/base/carousel'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { IndicatorButton } from './indicator-button'
|
||||
|
||||
export type BannerData = {
|
||||
id: string
|
||||
content: {
|
||||
'category': string
|
||||
'title': string
|
||||
'description': string
|
||||
'img-src': string
|
||||
}
|
||||
status: 'enabled' | 'disabled'
|
||||
link: string
|
||||
created_at: number
|
||||
}
|
||||
|
||||
type BannerItemProps = {
|
||||
banner: BannerData
|
||||
autoplayDelay: number
|
||||
isPaused?: boolean
|
||||
}
|
||||
|
||||
const RESPONSIVE_BREAKPOINT = 1200
|
||||
const MAX_RESPONSIVE_WIDTH = 600
|
||||
const INDICATOR_WIDTH = 20
|
||||
const INDICATOR_GAP = 8
|
||||
const MIN_VIEW_MORE_WIDTH = 480
|
||||
|
||||
export const BannerItem: FC<BannerItemProps> = ({ banner, autoplayDelay, isPaused = false }) => {
|
||||
const { t } = useTranslation()
|
||||
const { api, selectedIndex } = useCarousel()
|
||||
const { category, title, description, 'img-src': imgSrc } = banner.content
|
||||
|
||||
const [resetKey, setResetKey] = useState(0)
|
||||
const textAreaRef = useRef<HTMLDivElement>(null)
|
||||
const [maxWidth, setMaxWidth] = useState<number | undefined>(undefined)
|
||||
|
||||
const slideInfo = useMemo(() => {
|
||||
const slides = api?.slideNodes() ?? []
|
||||
const totalSlides = slides.length
|
||||
const nextIndex = totalSlides > 0 ? (selectedIndex + 1) % totalSlides : 0
|
||||
return { slides, totalSlides, nextIndex }
|
||||
}, [api, selectedIndex])
|
||||
|
||||
const indicatorsWidth = useMemo(() => {
|
||||
const count = slideInfo.totalSlides
|
||||
if (count === 0)
|
||||
return 0
|
||||
// Calculate: indicator buttons + gaps + extra spacing (3 * 20px for divider and padding)
|
||||
return (count + 2) * INDICATOR_WIDTH + (count - 1) * INDICATOR_GAP
|
||||
}, [slideInfo.totalSlides])
|
||||
|
||||
const viewMoreStyle = useMemo(() => {
|
||||
if (!maxWidth)
|
||||
return undefined
|
||||
return {
|
||||
maxWidth: `${maxWidth}px`,
|
||||
minWidth: indicatorsWidth ? `${Math.min(maxWidth - indicatorsWidth, MIN_VIEW_MORE_WIDTH)}px` : undefined,
|
||||
}
|
||||
}, [maxWidth, indicatorsWidth])
|
||||
|
||||
const responsiveStyle = useMemo(
|
||||
() => (maxWidth !== undefined ? { maxWidth: `${maxWidth}px` } : undefined),
|
||||
[maxWidth],
|
||||
)
|
||||
|
||||
const incrementResetKey = useCallback(() => setResetKey(prev => prev + 1), [])
|
||||
|
||||
useEffect(() => {
|
||||
const updateMaxWidth = () => {
|
||||
if (window.innerWidth < RESPONSIVE_BREAKPOINT && textAreaRef.current) {
|
||||
const textAreaWidth = textAreaRef.current.offsetWidth
|
||||
setMaxWidth(Math.min(textAreaWidth, MAX_RESPONSIVE_WIDTH))
|
||||
}
|
||||
else {
|
||||
setMaxWidth(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
updateMaxWidth()
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateMaxWidth)
|
||||
if (textAreaRef.current)
|
||||
resizeObserver.observe(textAreaRef.current)
|
||||
|
||||
window.addEventListener('resize', updateMaxWidth)
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
window.removeEventListener('resize', updateMaxWidth)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
incrementResetKey()
|
||||
}, [selectedIndex, incrementResetKey])
|
||||
|
||||
const handleBannerClick = useCallback(() => {
|
||||
incrementResetKey()
|
||||
if (banner.link)
|
||||
window.open(banner.link, '_blank', 'noopener,noreferrer')
|
||||
}, [banner.link, incrementResetKey])
|
||||
|
||||
const handleIndicatorClick = useCallback((index: number) => {
|
||||
incrementResetKey()
|
||||
api?.scrollTo(index)
|
||||
}, [api, incrementResetKey])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex w-full min-w-[784px] cursor-pointer overflow-hidden rounded-2xl bg-components-panel-on-panel-item-bg pr-[288px] transition-shadow hover:shadow-md"
|
||||
onClick={handleBannerClick}
|
||||
>
|
||||
{/* Left content area */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex h-full flex-col gap-3 py-6 pl-8 pr-0">
|
||||
{/* Text section */}
|
||||
<div className="flex min-h-24 flex-wrap items-end gap-1 py-1">
|
||||
{/* Title area */}
|
||||
<div
|
||||
ref={textAreaRef}
|
||||
className="flex min-w-[480px] max-w-[680px] flex-[1_0_0] flex-col pr-4"
|
||||
style={responsiveStyle}
|
||||
>
|
||||
<p className="title-4xl-semi-bold line-clamp-1 text-dify-logo-dify-logo-blue">
|
||||
{category}
|
||||
</p>
|
||||
<p className="title-4xl-semi-bold line-clamp-2 text-dify-logo-dify-logo-black">
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
{/* Description area */}
|
||||
<div
|
||||
className="min-w-60 max-w-[600px] flex-[1_0_0] self-end overflow-hidden py-1 pr-4"
|
||||
style={responsiveStyle}
|
||||
>
|
||||
<p className="body-sm-regular line-clamp-4 overflow-hidden text-text-tertiary">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions section */}
|
||||
<div className="flex items-center gap-1">
|
||||
{/* View more button */}
|
||||
<div
|
||||
className="flex min-w-[480px] max-w-[680px] flex-[1_0_0] items-center gap-[6px] py-1 pr-8"
|
||||
style={viewMoreStyle}
|
||||
>
|
||||
<div className="flex h-4 w-4 items-center justify-center rounded-full bg-text-accent p-[2px]">
|
||||
<RiArrowRightLine className="h-3 w-3 text-text-primary-on-surface" />
|
||||
</div>
|
||||
<span className="system-sm-semibold-uppercase text-text-accent">
|
||||
{t('banner.viewMore', { ns: 'explore' })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn('flex max-w-[600px] flex-[1_0_0] items-center gap-2 py-1 pr-10', maxWidth ? '' : 'min-w-60')}
|
||||
style={responsiveStyle}
|
||||
>
|
||||
{/* Slide navigation indicators */}
|
||||
<div className="flex items-center gap-2">
|
||||
{slideInfo.slides.map((_: unknown, index: number) => (
|
||||
<IndicatorButton
|
||||
key={index}
|
||||
index={index}
|
||||
selectedIndex={selectedIndex}
|
||||
isNextSlide={index === slideInfo.nextIndex}
|
||||
autoplayDelay={autoplayDelay}
|
||||
resetKey={resetKey}
|
||||
isPaused={isPaused}
|
||||
onClick={() => handleIndicatorClick(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="hidden h-[1px] flex-1 bg-divider-regular min-[1380px]:block" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right image area */}
|
||||
<div className="absolute right-0 top-0 flex h-full items-center p-2">
|
||||
<img
|
||||
src={imgSrc}
|
||||
alt={title}
|
||||
className="aspect-[4/3] h-full max-w-[296px] rounded-xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import type { FC } from 'react'
|
||||
import type { BannerData } from './banner-item'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Carousel } from '@/app/components/base/carousel'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useGetBanners } from '@/service/use-explore'
|
||||
import Loading from '../../base/loading'
|
||||
import { BannerItem } from './banner-item'
|
||||
|
||||
const AUTOPLAY_DELAY = 5000
|
||||
const MIN_LOADING_HEIGHT = 168
|
||||
const RESIZE_DEBOUNCE_DELAY = 50
|
||||
|
||||
const LoadingState: FC = () => (
|
||||
<div
|
||||
className="flex items-center justify-center rounded-2xl bg-components-panel-on-panel-item-bg shadow-md"
|
||||
style={{ minHeight: MIN_LOADING_HEIGHT }}
|
||||
>
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
|
||||
const Banner: FC = () => {
|
||||
const locale = useLocale()
|
||||
const { data: banners, isLoading, isError } = useGetBanners(locale)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [isResizing, setIsResizing] = useState(false)
|
||||
const resizeTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
const enabledBanners = useMemo(
|
||||
() => banners?.filter((banner: BannerData) => banner.status === 'enabled') ?? [],
|
||||
[banners],
|
||||
)
|
||||
|
||||
const isPaused = isHovered || isResizing
|
||||
|
||||
// Handle window resize to pause animation
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsResizing(true)
|
||||
|
||||
if (resizeTimerRef.current)
|
||||
clearTimeout(resizeTimerRef.current)
|
||||
|
||||
resizeTimerRef.current = setTimeout(() => {
|
||||
setIsResizing(false)
|
||||
}, RESIZE_DEBOUNCE_DELAY)
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (resizeTimerRef.current)
|
||||
clearTimeout(resizeTimerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (isLoading)
|
||||
return <LoadingState />
|
||||
|
||||
if (isError || enabledBanners.length === 0)
|
||||
return null
|
||||
|
||||
return (
|
||||
<Carousel
|
||||
opts={{ loop: true }}
|
||||
plugins={[
|
||||
Carousel.Plugin.Autoplay({
|
||||
delay: AUTOPLAY_DELAY,
|
||||
stopOnInteraction: false,
|
||||
stopOnMouseEnter: true,
|
||||
}),
|
||||
]}
|
||||
className="rounded-2xl"
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<Carousel.Content>
|
||||
{enabledBanners.map((banner: BannerData) => (
|
||||
<Carousel.Item key={banner.id}>
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={AUTOPLAY_DELAY}
|
||||
isPaused={isPaused}
|
||||
/>
|
||||
</Carousel.Item>
|
||||
))}
|
||||
</Carousel.Content>
|
||||
</Carousel>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Banner)
|
||||
@@ -1,111 +0,0 @@
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type IndicatorButtonProps = {
|
||||
index: number
|
||||
selectedIndex: number
|
||||
isNextSlide: boolean
|
||||
autoplayDelay: number
|
||||
resetKey: number
|
||||
isPaused?: boolean
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const PROGRESS_MAX = 100
|
||||
const DEGREES_PER_PERCENT = 3.6
|
||||
|
||||
export const IndicatorButton: FC<IndicatorButtonProps> = ({
|
||||
index,
|
||||
selectedIndex,
|
||||
isNextSlide,
|
||||
autoplayDelay,
|
||||
resetKey,
|
||||
isPaused = false,
|
||||
onClick,
|
||||
}) => {
|
||||
const [progress, setProgress] = useState(0)
|
||||
const frameIdRef = useRef<number | undefined>(undefined)
|
||||
const startTimeRef = useRef(0)
|
||||
|
||||
const isActive = index === selectedIndex
|
||||
const shouldAnimate = !document.hidden && !isPaused
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNextSlide) {
|
||||
setProgress(0)
|
||||
if (frameIdRef.current)
|
||||
cancelAnimationFrame(frameIdRef.current)
|
||||
return
|
||||
}
|
||||
|
||||
setProgress(0)
|
||||
startTimeRef.current = Date.now()
|
||||
|
||||
const animate = () => {
|
||||
if (!document.hidden && !isPaused) {
|
||||
const elapsed = Date.now() - startTimeRef.current
|
||||
const newProgress = Math.min((elapsed / autoplayDelay) * PROGRESS_MAX, PROGRESS_MAX)
|
||||
setProgress(newProgress)
|
||||
|
||||
if (newProgress < PROGRESS_MAX)
|
||||
frameIdRef.current = requestAnimationFrame(animate)
|
||||
}
|
||||
else {
|
||||
frameIdRef.current = requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldAnimate)
|
||||
frameIdRef.current = requestAnimationFrame(animate)
|
||||
|
||||
return () => {
|
||||
if (frameIdRef.current)
|
||||
cancelAnimationFrame(frameIdRef.current)
|
||||
}
|
||||
}, [isNextSlide, autoplayDelay, resetKey, isPaused])
|
||||
|
||||
const handleClick = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
onClick()
|
||||
}, [onClick])
|
||||
|
||||
const progressDegrees = progress * DEGREES_PER_PERCENT
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
'system-2xs-semibold-uppercase relative flex h-[18px] w-[20px] items-center justify-center rounded-[7px] border border-divider-subtle p-[2px] text-center transition-colors',
|
||||
isActive
|
||||
? 'bg-text-primary text-components-panel-on-panel-item-bg'
|
||||
: 'bg-components-panel-on-panel-item-bg text-text-tertiary hover:text-text-secondary',
|
||||
)}
|
||||
>
|
||||
{/* progress border for next slide */}
|
||||
{isNextSlide && !isActive && (
|
||||
<span
|
||||
key={resetKey}
|
||||
className="absolute inset-[-1px] rounded-[7px]"
|
||||
style={{
|
||||
background: `conic-gradient(
|
||||
from 0deg,
|
||||
var(--color-text-primary) ${progressDegrees}deg,
|
||||
transparent ${progressDegrees}deg
|
||||
)`,
|
||||
WebkitMask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
|
||||
WebkitMaskComposite: 'xor',
|
||||
mask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
|
||||
maskComposite: 'exclude',
|
||||
padding: '1px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* number content */}
|
||||
<span className="relative z-10">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -29,7 +29,7 @@ const Category: FC<ICategoryProps> = ({
|
||||
const isAllCategories = !list.includes(value as AppCategory) || value === allCategoriesEn
|
||||
|
||||
const itemClassName = (isSelected: boolean) => cn(
|
||||
'system-sm-medium flex h-7 cursor-pointer items-center rounded-lg border border-transparent px-3 text-text-tertiary hover:bg-components-main-nav-nav-button-bg-active',
|
||||
'flex h-[32px] cursor-pointer items-center rounded-lg border-[0.5px] border-transparent px-3 py-[7px] font-medium leading-[18px] text-text-tertiary hover:bg-components-main-nav-nav-button-bg-active',
|
||||
isSelected && 'border-components-main-nav-nav-button-border bg-components-main-nav-nav-button-bg-active text-components-main-nav-nav-button-text-active shadow-xs',
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { CurrentTryAppParams } from '@/context/explore-context'
|
||||
import type { InstalledApp } from '@/models/explore'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
@@ -42,16 +41,6 @@ const Explore: FC<IExploreProps> = ({
|
||||
return router.replace('/datasets')
|
||||
}, [isCurrentWorkspaceDatasetOperator])
|
||||
|
||||
const [currentTryAppParams, setCurrentTryAppParams] = useState<CurrentTryAppParams | undefined>(undefined)
|
||||
const [isShowTryAppPanel, setIsShowTryAppPanel] = useState(false)
|
||||
const setShowTryAppPanel = (showTryAppPanel: boolean, params?: CurrentTryAppParams) => {
|
||||
if (showTryAppPanel)
|
||||
setCurrentTryAppParams(params)
|
||||
else
|
||||
setCurrentTryAppParams(undefined)
|
||||
setIsShowTryAppPanel(showTryAppPanel)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full overflow-hidden border-t border-divider-regular bg-background-body">
|
||||
<ExploreContext.Provider
|
||||
@@ -64,9 +53,6 @@ const Explore: FC<IExploreProps> = ({
|
||||
setInstalledApps,
|
||||
isFetchingInstalledApps,
|
||||
setIsFetchingInstalledApps,
|
||||
currentApp: currentTryAppParams,
|
||||
isShowTryAppPanel,
|
||||
setShowTryAppPanel,
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { AccessMode } from '@/models/access-control'
|
||||
import type { AppData } from '@/models/share'
|
||||
import * as React from 'react'
|
||||
import { useEffect } from 'react'
|
||||
@@ -63,8 +62,8 @@ const InstalledApp: FC<IInstalledAppProps> = ({
|
||||
if (appMeta)
|
||||
updateWebAppMeta(appMeta)
|
||||
if (webAppAccessMode)
|
||||
updateWebAppAccessMode((webAppAccessMode as { accessMode: AccessMode }).accessMode)
|
||||
updateUserCanAccessApp(Boolean(userCanAccessApp && (userCanAccessApp as { result: boolean })?.result))
|
||||
updateWebAppAccessMode(webAppAccessMode.accessMode)
|
||||
updateUserCanAccessApp(Boolean(userCanAccessApp && userCanAccessApp?.result))
|
||||
}, [installedApp, appMeta, appParams, updateAppInfo, updateAppParams, updateUserCanAccessApp, updateWebAppMeta, userCanAccessApp, webAppAccessMode, updateWebAppAccessMode])
|
||||
|
||||
if (appParamsError) {
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function AppNavItem({
|
||||
<>
|
||||
<div className="flex w-0 grow items-center space-x-2">
|
||||
<AppIcon size="tiny" iconType={icon_type} icon={icon} background={icon_background} imageUrl={icon_url} />
|
||||
<div className="system-sm-regular truncate text-components-menu-item-text" title={name}>{name}</div>
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap" title={name}>{name}</div>
|
||||
</div>
|
||||
<div className="h-6 shrink-0" onClick={e => e.stopPropagation()}>
|
||||
<ItemOperation
|
||||
|
||||
@@ -72,7 +72,7 @@ const renderWithContext = (installedApps: InstalledApp[] = []) => {
|
||||
setInstalledApps: vi.fn(),
|
||||
isFetchingInstalledApps: false,
|
||||
setIsFetchingInstalledApps: vi.fn(),
|
||||
} as any}
|
||||
}}
|
||||
>
|
||||
<SideBar controlUpdateInstalledApps={0} />
|
||||
</ExploreContext.Provider>,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { RiAppsFill, RiExpandRightLine, RiLayoutLeft2Line } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import Link from 'next/link'
|
||||
import { useSelectedLayoutSegments } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
@@ -16,7 +14,6 @@ import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/s
|
||||
import { cn } from '@/utils/classnames'
|
||||
import Toast from '../../base/toast'
|
||||
import Item from './app-nav-item'
|
||||
import NoApps from './no-apps'
|
||||
|
||||
const SelectedDiscoveryIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="current" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -48,9 +45,6 @@ const SideBar: FC<IExploreSideBarProps> = ({
|
||||
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
const [isFold, {
|
||||
toggle: toggleIsFold,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
const [currId, setCurrId] = useState('')
|
||||
@@ -90,31 +84,22 @@ const SideBar: FC<IExploreSideBarProps> = ({
|
||||
|
||||
const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length
|
||||
return (
|
||||
<div className={cn('relative w-fit shrink-0 cursor-pointer px-3 pt-6 sm:w-[240px]', isFold && 'sm:w-[56px]')}>
|
||||
<div className="w-fit shrink-0 cursor-pointer border-r border-divider-burn px-4 pt-6 sm:w-[216px]">
|
||||
<div className={cn(isDiscoverySelected ? 'text-text-accent' : 'text-text-tertiary')}>
|
||||
<Link
|
||||
href="/explore/apps"
|
||||
className={cn(isDiscoverySelected ? 'bg-state-base-active' : 'hover:bg-state-base-hover', 'flex h-8 items-center gap-2 rounded-lg px-1 mobile:w-fit mobile:justify-center pc:w-full pc:justify-start')}
|
||||
className={cn(isDiscoverySelected ? ' bg-components-main-nav-nav-button-bg-active' : 'font-medium hover:bg-state-base-hover', 'flex h-9 items-center gap-2 rounded-lg px-3 mobile:w-fit mobile:justify-center mobile:px-2 pc:w-full pc:justify-start')}
|
||||
style={isDiscoverySelected ? { boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)' } : {}}
|
||||
>
|
||||
<div className="flex size-6 shrink-0 items-center justify-center rounded-md bg-components-icon-bg-blue-solid">
|
||||
<RiAppsFill className="size-3.5 text-components-avatar-shape-fill-stop-100" />
|
||||
</div>
|
||||
{!isMobile && !isFold && <div className={cn('truncate', isDiscoverySelected ? 'system-sm-semibold text-components-menu-item-text-active' : 'system-sm-regular text-components-menu-item-text')}>{t('sidebar.title', { ns: 'explore' })}</div>}
|
||||
{isDiscoverySelected ? <SelectedDiscoveryIcon /> : <DiscoveryIcon />}
|
||||
{!isMobile && <div className="text-sm">{t('sidebar.discovery', { ns: 'explore' })}</div>}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{installedApps.length === 0 && !isMobile && !isFold
|
||||
&& (
|
||||
<div className="mt-5">
|
||||
<NoApps />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{installedApps.length > 0 && (
|
||||
<div className="mt-5">
|
||||
{!isMobile && !isFold && <p className="system-xs-medium-uppercase mb-1.5 break-all pl-2 uppercase text-text-tertiary mobile:px-0">{t('sidebar.webApps', { ns: 'explore' })}</p>}
|
||||
<div className="mt-10">
|
||||
<p className="break-all pl-2 text-xs font-medium uppercase text-text-tertiary mobile:px-0">{t('sidebar.workspace', { ns: 'explore' })}</p>
|
||||
<div
|
||||
className="space-y-0.5 overflow-y-auto overflow-x-hidden"
|
||||
className="mt-3 space-y-1 overflow-y-auto overflow-x-hidden"
|
||||
style={{
|
||||
height: 'calc(100vh - 250px)',
|
||||
}}
|
||||
@@ -122,7 +107,7 @@ const SideBar: FC<IExploreSideBarProps> = ({
|
||||
{installedApps.map(({ id, is_pinned, uninstallable, app: { name, icon_type, icon, icon_url, icon_background } }, index) => (
|
||||
<React.Fragment key={id}>
|
||||
<Item
|
||||
isMobile={isMobile || isFold}
|
||||
isMobile={isMobile}
|
||||
name={name}
|
||||
icon_type={icon_type}
|
||||
icon={icon}
|
||||
@@ -144,17 +129,6 @@ const SideBar: FC<IExploreSideBarProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isMobile && (
|
||||
<div className="absolute bottom-3 left-3 flex size-8 cursor-pointer items-center justify-center text-text-tertiary" onClick={toggleIsFold}>
|
||||
{isFold
|
||||
? <RiExpandRightLine className="size-4.5" />
|
||||
: (
|
||||
<RiLayoutLeft2Line className="size-4.5" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showConfirm && (
|
||||
<Confirm
|
||||
title={t('sidebar.delete.title', { ns: 'explore' })}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import s from './style.module.css'
|
||||
|
||||
const i18nPrefix = 'sidebar.noApps'
|
||||
|
||||
const NoApps: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
return (
|
||||
<div className="rounded-xl bg-background-default-subtle p-4">
|
||||
<div className={cn('h-[35px] w-[86px] bg-contain bg-center bg-no-repeat', theme === Theme.dark ? s.dark : s.light)}></div>
|
||||
<div className="system-sm-semibold mt-2 text-text-secondary">{t(`${i18nPrefix}.title`, { ns: 'explore' })}</div>
|
||||
<div className="system-xs-regular my-1 text-text-tertiary">{t(`${i18nPrefix}.description`, { ns: 'explore' })}</div>
|
||||
<a className="system-xs-regular text-text-accent" target="_blank" rel="noopener noreferrer" href="https://docs.dify.ai/en/guides/application-publishing/launch-your-webapp-quickly/README">{t(`${i18nPrefix}.learnMore`, { ns: 'explore' })}</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(NoApps)
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB |
@@ -1,7 +0,0 @@
|
||||
.light {
|
||||
background-image: url('./no-web-apps-light.png');
|
||||
}
|
||||
|
||||
.dark {
|
||||
background-image: url('./no-web-apps-dark.png');
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { TryAppInfo } from '@/service/try-app'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AppTypeIcon } from '@/app/components/app/type-selector'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import useGetRequirements from './use-get-requirements'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
appDetail: TryAppInfo
|
||||
category?: string
|
||||
className?: string
|
||||
onCreate: () => void
|
||||
}
|
||||
|
||||
const headerClassName = 'system-sm-semibold-uppercase text-text-secondary mb-3'
|
||||
|
||||
const AppInfo: FC<Props> = ({
|
||||
appId,
|
||||
className,
|
||||
category,
|
||||
appDetail,
|
||||
onCreate,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const mode = appDetail?.mode
|
||||
const { requirements } = useGetRequirements({ appDetail, appId })
|
||||
return (
|
||||
<div className={cn('flex h-full flex-col px-4 pt-2', className)}>
|
||||
{/* name and icon */}
|
||||
<div className="flex shrink-0 grow-0 items-center gap-3">
|
||||
<div className="relative shrink-0">
|
||||
<AppIcon
|
||||
size="large"
|
||||
iconType={appDetail.site.icon_type}
|
||||
icon={appDetail.site.icon}
|
||||
background={appDetail.site.icon_background}
|
||||
imageUrl={appDetail.site.icon_url}
|
||||
/>
|
||||
<AppTypeIcon
|
||||
wrapperClassName="absolute -bottom-0.5 -right-0.5 w-4 h-4 shadow-sm"
|
||||
className="h-3 w-3"
|
||||
type={mode}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-0 grow py-[1px]">
|
||||
<div className="flex items-center text-sm font-semibold leading-5 text-text-secondary">
|
||||
<div className="truncate" title={appDetail.name}>{appDetail.name}</div>
|
||||
</div>
|
||||
<div className="flex items-center text-[10px] font-medium leading-[18px] text-text-tertiary">
|
||||
{mode === 'advanced-chat' && <div className="truncate">{t('types.advanced', { ns: 'app' }).toUpperCase()}</div>}
|
||||
{mode === 'chat' && <div className="truncate">{t('types.chatbot', { ns: 'app' }).toUpperCase()}</div>}
|
||||
{mode === 'agent-chat' && <div className="truncate">{t('types.agent', { ns: 'app' }).toUpperCase()}</div>}
|
||||
{mode === 'workflow' && <div className="truncate">{t('types.workflow', { ns: 'app' }).toUpperCase()}</div>}
|
||||
{mode === 'completion' && <div className="truncate">{t('types.completion', { ns: 'app' }).toUpperCase()}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{appDetail.description && (
|
||||
<div className="system-sm-regular mt-[14px] shrink-0 text-text-secondary">{appDetail.description}</div>
|
||||
)}
|
||||
<Button variant="primary" className="mt-3 flex w-full max-w-full" onClick={onCreate}>
|
||||
<RiAddLine className="mr-1 size-4 shrink-0" />
|
||||
<span className="truncate">{t('tryApp.createFromSampleApp', { ns: 'explore' })}</span>
|
||||
</Button>
|
||||
|
||||
{category && (
|
||||
<div className="mt-6 shrink-0">
|
||||
<div className={headerClassName}>{t('tryApp.category', { ns: 'explore' })}</div>
|
||||
<div className="system-md-regular text-text-secondary">{category}</div>
|
||||
</div>
|
||||
)}
|
||||
{requirements.length > 0 && (
|
||||
<div className="mt-5 grow overflow-y-auto">
|
||||
<div className={headerClassName}>{t('tryApp.requirements', { ns: 'explore' })}</div>
|
||||
<div className="space-y-0.5">
|
||||
{requirements.map(item => (
|
||||
<div className="flex items-center space-x-2 py-1" key={item.name}>
|
||||
<div className="size-5 rounded-md bg-cover shadow-xs" style={{ backgroundImage: `url(${item.iconUrl})` }} />
|
||||
<div className="system-md-regular w-0 grow truncate text-text-secondary">{item.name}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(AppInfo)
|
||||
@@ -1,78 +0,0 @@
|
||||
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
|
||||
import type { ToolNodeType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import type { TryAppInfo } from '@/service/try-app'
|
||||
import type { AgentTool } from '@/types/app'
|
||||
import { uniqBy } from 'es-toolkit/compat'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { MARKETPLACE_API_PREFIX } from '@/config'
|
||||
import { useGetTryAppFlowPreview } from '@/service/use-try-app'
|
||||
|
||||
type Params = {
|
||||
appDetail: TryAppInfo
|
||||
appId: string
|
||||
}
|
||||
|
||||
type RequirementItem = {
|
||||
name: string
|
||||
iconUrl: string
|
||||
}
|
||||
const getIconUrl = (provider: string, tool: string) => {
|
||||
return `${MARKETPLACE_API_PREFIX}/plugins/${provider}/${tool}/icon`
|
||||
}
|
||||
|
||||
const useGetRequirements = ({ appDetail, appId }: Params) => {
|
||||
const isBasic = ['chat', 'completion', 'agent-chat'].includes(appDetail.mode)
|
||||
const isAgent = appDetail.mode === 'agent-chat'
|
||||
const isAdvanced = !isBasic
|
||||
const { data: flowData } = useGetTryAppFlowPreview(appId, isBasic)
|
||||
|
||||
const requirements: RequirementItem[] = []
|
||||
if (isBasic) {
|
||||
const modelProviderAndName = appDetail.model_config.model.provider.split('/')
|
||||
const name = appDetail.model_config.model.provider.split('/').pop() || ''
|
||||
requirements.push({
|
||||
name,
|
||||
iconUrl: getIconUrl(modelProviderAndName[0], modelProviderAndName[1]),
|
||||
})
|
||||
}
|
||||
if (isAgent) {
|
||||
requirements.push(...appDetail.model_config.agent_mode.tools.filter(data => (data as AgentTool).enabled).map((data) => {
|
||||
const tool = data as AgentTool
|
||||
const modelProviderAndName = tool.provider_id.split('/')
|
||||
return {
|
||||
name: tool.tool_label,
|
||||
iconUrl: getIconUrl(modelProviderAndName[0], modelProviderAndName[1]),
|
||||
}
|
||||
}))
|
||||
}
|
||||
if (isAdvanced && flowData && flowData?.graph?.nodes?.length > 0) {
|
||||
const nodes = flowData.graph.nodes
|
||||
const llmNodes = nodes.filter(node => node.data.type === BlockEnum.LLM)
|
||||
requirements.push(...llmNodes.map((node) => {
|
||||
const data = node.data as LLMNodeType
|
||||
const modelProviderAndName = data.model.provider.split('/')
|
||||
return {
|
||||
name: data.model.name,
|
||||
iconUrl: getIconUrl(modelProviderAndName[0], modelProviderAndName[1]),
|
||||
}
|
||||
}))
|
||||
|
||||
const toolNodes = nodes.filter(node => node.data.type === BlockEnum.Tool)
|
||||
requirements.push(...toolNodes.map((node) => {
|
||||
const data = node.data as ToolNodeType
|
||||
const toolProviderAndName = data.provider_id.split('/')
|
||||
return {
|
||||
name: data.tool_label,
|
||||
iconUrl: getIconUrl(toolProviderAndName[0], toolProviderAndName[1]),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
const uniqueRequirements = uniqBy(requirements, 'name')
|
||||
|
||||
return {
|
||||
requirements: uniqueRequirements,
|
||||
}
|
||||
}
|
||||
|
||||
export default useGetRequirements
|
||||
@@ -1,101 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { TryAppInfo } from '@/service/try-app'
|
||||
import { RiResetLeftLine } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Alert from '@/app/components/base/alert'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrapper'
|
||||
import {
|
||||
EmbeddedChatbotContext,
|
||||
} from '@/app/components/base/chat/embedded-chatbot/context'
|
||||
import {
|
||||
useEmbeddedChatbot,
|
||||
} from '@/app/components/base/chat/embedded-chatbot/hooks'
|
||||
import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { useThemeContext } from '../../../base/chat/embedded-chatbot/theme/theme-context'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
appDetail: TryAppInfo
|
||||
className: string
|
||||
}
|
||||
|
||||
const TryApp: FC<Props> = ({
|
||||
appId,
|
||||
appDetail,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
const themeBuilder = useThemeContext()
|
||||
const { removeConversationIdInfo, ...chatData } = useEmbeddedChatbot(AppSourceType.tryApp, appId)
|
||||
const currentConversationId = chatData.currentConversationId
|
||||
const inputsForms = chatData.inputsForms
|
||||
useEffect(() => {
|
||||
if (appId)
|
||||
removeConversationIdInfo(appId)
|
||||
}, [appId])
|
||||
const [isHideTryNotice, {
|
||||
setTrue: hideTryNotice,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleNewConversation = () => {
|
||||
removeConversationIdInfo(appId)
|
||||
chatData.handleNewConversation()
|
||||
}
|
||||
return (
|
||||
<EmbeddedChatbotContext.Provider value={{
|
||||
...chatData,
|
||||
disableFeedback: true,
|
||||
isMobile,
|
||||
themeBuilder,
|
||||
} as any}
|
||||
>
|
||||
<div className={cn('flex h-full flex-col rounded-2xl bg-background-section-burn', className)}>
|
||||
<div className="flex shrink-0 justify-between p-3">
|
||||
<div className="flex grow items-center space-x-2">
|
||||
<AppIcon
|
||||
size="large"
|
||||
iconType={appDetail.site.icon_type}
|
||||
icon={appDetail.site.icon}
|
||||
background={appDetail.site.icon_background}
|
||||
imageUrl={appDetail.site.icon_url}
|
||||
/>
|
||||
<div className="system-md-semibold grow truncate text-text-primary" title={appDetail.name}>{appDetail.name}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{currentConversationId && (
|
||||
<Tooltip
|
||||
popupContent={t('chat.resetChat', { ns: 'share' })}
|
||||
>
|
||||
<ActionButton size="l" onClick={handleNewConversation}>
|
||||
<RiResetLeftLine className="h-[18px] w-[18px]" />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{currentConversationId && inputsForms.length > 0 && (
|
||||
<ViewFormDropdown />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto mt-4 flex h-[0] w-[769px] grow flex-col">
|
||||
{!isHideTryNotice && (
|
||||
<Alert className="mb-4 shrink-0" message={t('tryApp.tryInfo', { ns: 'explore' })} onHide={hideTryNotice} />
|
||||
)}
|
||||
<ChatWrapper />
|
||||
</div>
|
||||
</div>
|
||||
</EmbeddedChatbotContext.Provider>
|
||||
)
|
||||
}
|
||||
export default React.memo(TryApp)
|
||||
@@ -1,44 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { AppData } from '@/models/share'
|
||||
import type { TryAppInfo } from '@/service/try-app'
|
||||
import * as React from 'react'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import Chat from './chat'
|
||||
import TextGeneration from './text-generation'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
appDetail: TryAppInfo
|
||||
}
|
||||
|
||||
const TryApp: FC<Props> = ({
|
||||
appId,
|
||||
appDetail,
|
||||
}) => {
|
||||
const mode = appDetail?.mode
|
||||
const isChat = ['chat', 'advanced-chat', 'agent-chat'].includes(mode!)
|
||||
const isCompletion = !isChat
|
||||
|
||||
useDocumentTitle(appDetail?.site?.title || '')
|
||||
return (
|
||||
<div className="flex h-full w-full">
|
||||
{isChat && (
|
||||
<Chat appId={appId} appDetail={appDetail} className="h-full grow" />
|
||||
)}
|
||||
{isCompletion && (
|
||||
<TextGeneration
|
||||
appId={appId}
|
||||
className="h-full grow"
|
||||
isWorkflow={mode === 'workflow'}
|
||||
appData={{
|
||||
app_id: appId,
|
||||
custom_config: {},
|
||||
...appDetail,
|
||||
} as AppData}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(TryApp)
|
||||
@@ -1,261 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { Task } from '../../../share/text-generation/types'
|
||||
import type { MoreLikeThisConfig, PromptConfig, TextToSpeechConfig } from '@/models/debug'
|
||||
import type { AppData, SiteInfo } from '@/models/share'
|
||||
import type { VisionFile, VisionSettings } from '@/types/app'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Alert from '@/app/components/base/alert'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Res from '@/app/components/share/text-generation/result'
|
||||
import { TaskStatus } from '@/app/components/share/text-generation/types'
|
||||
import { appDefaultIconBackground } from '@/config'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
import { useGetTryAppParams } from '@/service/use-try-app'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { userInputsFormToPromptVariables } from '@/utils/model-config'
|
||||
import RunOnce from '../../../share/text-generation/run-once'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
className?: string
|
||||
isWorkflow?: boolean
|
||||
appData: AppData | null
|
||||
}
|
||||
|
||||
const TextGeneration: FC<Props> = ({
|
||||
appId,
|
||||
className,
|
||||
isWorkflow,
|
||||
appData,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const media = useBreakpoints()
|
||||
const isPC = media === MediaType.pc
|
||||
|
||||
const [inputs, doSetInputs] = useState<Record<string, any>>({})
|
||||
const inputsRef = useRef<Record<string, any>>(inputs)
|
||||
const setInputs = useCallback((newInputs: Record<string, any>) => {
|
||||
doSetInputs(newInputs)
|
||||
inputsRef.current = newInputs
|
||||
}, [])
|
||||
|
||||
const updateAppInfo = useWebAppStore(s => s.updateAppInfo)
|
||||
const { data: tryAppParams } = useGetTryAppParams(appId)
|
||||
|
||||
const updateAppParams = useWebAppStore(s => s.updateAppParams)
|
||||
const appParams = useWebAppStore(s => s.appParams)
|
||||
const [siteInfo, setSiteInfo] = useState<SiteInfo | null>(null)
|
||||
const [promptConfig, setPromptConfig] = useState<PromptConfig | null>(null)
|
||||
const [customConfig, setCustomConfig] = useState<Record<string, any> | null>(null)
|
||||
const [moreLikeThisConfig, setMoreLikeThisConfig] = useState<MoreLikeThisConfig | null>(null)
|
||||
const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig | null>(null)
|
||||
const [controlSend, setControlSend] = useState(0)
|
||||
const [visionConfig, setVisionConfig] = useState<VisionSettings>({
|
||||
enabled: false,
|
||||
number_limits: 2,
|
||||
detail: Resolution.low,
|
||||
transfer_methods: [TransferMethod.local_file],
|
||||
})
|
||||
const [completionFiles, setCompletionFiles] = useState<VisionFile[]>([])
|
||||
const [isShowResultPanel, { setTrue: doShowResultPanel, setFalse: hideResultPanel }] = useBoolean(false)
|
||||
const showResultPanel = () => {
|
||||
// fix: useClickAway hideResSidebar will close sidebar
|
||||
setTimeout(() => {
|
||||
doShowResultPanel()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const handleSend = () => {
|
||||
setControlSend(Date.now())
|
||||
showResultPanel()
|
||||
}
|
||||
|
||||
const [resultExisted, setResultExisted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!appData)
|
||||
return
|
||||
updateAppInfo(appData)
|
||||
}, [appData, updateAppInfo])
|
||||
|
||||
useEffect(() => {
|
||||
if (!tryAppParams)
|
||||
return
|
||||
updateAppParams(tryAppParams)
|
||||
}, [tryAppParams, updateAppParams])
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!appData || !appParams)
|
||||
return
|
||||
const { site: siteInfo, custom_config } = appData
|
||||
setSiteInfo(siteInfo as SiteInfo)
|
||||
setCustomConfig(custom_config)
|
||||
|
||||
const { user_input_form, more_like_this, file_upload, text_to_speech }: any = appParams
|
||||
setVisionConfig({
|
||||
// legacy of image upload compatible
|
||||
...file_upload,
|
||||
transfer_methods: file_upload?.allowed_file_upload_methods || file_upload?.allowed_upload_methods,
|
||||
// legacy of image upload compatible
|
||||
image_file_size_limit: appParams?.system_parameters.image_file_size_limit,
|
||||
fileUploadConfig: appParams?.system_parameters,
|
||||
} as any)
|
||||
const prompt_variables = userInputsFormToPromptVariables(user_input_form)
|
||||
setPromptConfig({
|
||||
prompt_template: '', // placeholder for future
|
||||
prompt_variables,
|
||||
} as PromptConfig)
|
||||
setMoreLikeThisConfig(more_like_this)
|
||||
setTextToSpeechConfig(text_to_speech)
|
||||
})()
|
||||
}, [appData, appParams])
|
||||
|
||||
const [isCompleted, setIsCompleted] = useState(false)
|
||||
const handleCompleted = useCallback(() => {
|
||||
setIsCompleted(true)
|
||||
}, [])
|
||||
const [isHideTryNotice, {
|
||||
setTrue: hideTryNotice,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const renderRes = (task?: Task) => (
|
||||
<Res
|
||||
key={task?.id}
|
||||
isWorkflow={!!isWorkflow}
|
||||
isCallBatchAPI={false}
|
||||
isPC={isPC}
|
||||
isMobile={!isPC}
|
||||
appSourceType={AppSourceType.tryApp}
|
||||
appId={appId}
|
||||
isError={task?.status === TaskStatus.failed}
|
||||
promptConfig={promptConfig}
|
||||
moreLikeThisEnabled={!!moreLikeThisConfig?.enabled}
|
||||
inputs={inputs}
|
||||
controlSend={controlSend}
|
||||
onShowRes={showResultPanel}
|
||||
handleSaveMessage={noop}
|
||||
taskId={task?.id}
|
||||
onCompleted={handleCompleted}
|
||||
visionConfig={visionConfig}
|
||||
completionFiles={completionFiles}
|
||||
isShowTextToSpeech={!!textToSpeechConfig?.enabled}
|
||||
siteInfo={siteInfo}
|
||||
onRunStart={() => setResultExisted(true)}
|
||||
/>
|
||||
)
|
||||
|
||||
const renderResWrap = (
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-full flex-col',
|
||||
'rounded-r-2xl bg-chatbot-bg',
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
'flex h-0 grow flex-col overflow-y-auto p-6',
|
||||
)}
|
||||
>
|
||||
{isCompleted && !isHideTryNotice && (
|
||||
<Alert className="mb-3 shrink-0" message={t('tryApp.tryInfo', { ns: 'explore' })} onHide={hideTryNotice} />
|
||||
)}
|
||||
{renderRes()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (!siteInfo || !promptConfig) {
|
||||
return (
|
||||
<div className={cn('flex h-screen items-center', className)}>
|
||||
<Loading type="app" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'rounded-2xl border border-components-panel-border bg-background-section-burn',
|
||||
isPC && 'flex',
|
||||
!isPC && 'flex-col',
|
||||
'h-full rounded-2xl shadow-md',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{/* Left */}
|
||||
<div className={cn(
|
||||
'relative flex h-full shrink-0 flex-col',
|
||||
isPC && 'w-[600px] max-w-[50%]',
|
||||
'rounded-l-2xl bg-components-panel-bg',
|
||||
)}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className={cn('shrink-0 space-y-4 pb-2', isPC ? ' p-8 pb-0' : 'p-4 pb-0')}>
|
||||
<div className="flex items-center gap-3">
|
||||
<AppIcon
|
||||
size={isPC ? 'large' : 'small'}
|
||||
iconType={siteInfo.icon_type}
|
||||
icon={siteInfo.icon}
|
||||
background={siteInfo.icon_background || appDefaultIconBackground}
|
||||
imageUrl={siteInfo.icon_url}
|
||||
/>
|
||||
<div className="system-md-semibold grow truncate text-text-secondary">{siteInfo.title}</div>
|
||||
</div>
|
||||
{siteInfo.description && (
|
||||
<div className="system-xs-regular text-text-tertiary">{siteInfo.description}</div>
|
||||
)}
|
||||
</div>
|
||||
{/* form */}
|
||||
<div className={cn(
|
||||
'h-0 grow overflow-y-auto',
|
||||
isPC ? 'px-8' : 'px-4',
|
||||
!isPC && resultExisted && customConfig?.remove_webapp_brand && 'rounded-b-2xl border-b-[0.5px] border-divider-regular',
|
||||
)}
|
||||
>
|
||||
<RunOnce
|
||||
siteInfo={siteInfo}
|
||||
inputs={inputs}
|
||||
inputsRef={inputsRef}
|
||||
onInputsChange={setInputs}
|
||||
promptConfig={promptConfig}
|
||||
onSend={handleSend}
|
||||
visionConfig={visionConfig}
|
||||
onVisionFilesChange={setCompletionFiles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Result */}
|
||||
<div className={cn('h-full w-0 grow')}>
|
||||
{!isPC && (
|
||||
<div
|
||||
className={cn(
|
||||
isShowResultPanel
|
||||
? 'flex items-center justify-center p-2 pt-6'
|
||||
: 'absolute left-0 top-0 z-10 flex w-full items-center justify-center px-2 pb-[57px] pt-[3px]',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (isShowResultPanel)
|
||||
hideResultPanel()
|
||||
else
|
||||
showResultPanel()
|
||||
}}
|
||||
>
|
||||
<div className="h-1 w-8 cursor-grab rounded bg-divider-solid" />
|
||||
</div>
|
||||
)}
|
||||
{renderResWrap}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(TextGeneration)
|
||||
@@ -1,73 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Modal from '@/app/components/base/modal/index'
|
||||
import { useGetTryAppInfo } from '@/service/use-try-app'
|
||||
import Button from '../../base/button'
|
||||
import App from './app'
|
||||
import AppInfo from './app-info'
|
||||
import Preview from './preview'
|
||||
import Tab, { TypeEnum } from './tab'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
category?: string
|
||||
onClose: () => void
|
||||
onCreate: () => void
|
||||
}
|
||||
|
||||
const TryApp: FC<Props> = ({
|
||||
appId,
|
||||
category,
|
||||
onClose,
|
||||
onCreate,
|
||||
}) => {
|
||||
const [type, setType] = useState<TypeEnum>(TypeEnum.TRY)
|
||||
const { data: appDetail, isLoading } = useGetTryAppInfo(appId)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={onClose}
|
||||
className="h-[calc(100vh-32px)] min-w-[1280px] max-w-[calc(100vw-32px)] overflow-x-auto p-2"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Loading type="area" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex shrink-0 justify-between pl-4">
|
||||
<Tab
|
||||
value={type}
|
||||
onChange={setType}
|
||||
/>
|
||||
<Button
|
||||
size="large"
|
||||
variant="tertiary"
|
||||
className="flex size-7 items-center justify-center rounded-[10px] p-0 text-components-button-tertiary-text"
|
||||
onClick={onClose}
|
||||
>
|
||||
<RiCloseLine className="size-5" onClick={onClose} />
|
||||
</Button>
|
||||
</div>
|
||||
{/* Main content */}
|
||||
<div className="mt-2 flex h-0 grow justify-between space-x-2">
|
||||
{type === TypeEnum.TRY ? <App appId={appId} appDetail={appDetail!} /> : <Preview appId={appId} appDetail={appDetail!} />}
|
||||
<AppInfo
|
||||
className="w-[360px] shrink-0"
|
||||
appDetail={appDetail!}
|
||||
appId={appId}
|
||||
category={category}
|
||||
onCreate={onCreate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
export default React.memo(TryApp)
|
||||
@@ -1,364 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { Features as FeaturesData, FileUpload } from '@/app/components/base/features/types'
|
||||
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { ModelConfig } from '@/models/debug'
|
||||
import type { ModelConfig as BackendModelConfig, PromptVariable } from '@/types/app'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { clone } from 'es-toolkit/object'
|
||||
import * as React from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import Config from '@/app/components/app/configuration/config'
|
||||
import Debug from '@/app/components/app/configuration/debug'
|
||||
import { FeaturesProvider } from '@/app/components/base/features'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import { ANNOTATION_DEFAULT, DEFAULT_AGENT_SETTING, DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { PromptMode } from '@/models/debug'
|
||||
import { useAllToolProviders } from '@/service/use-tools'
|
||||
import { useGetTryAppDataSets, useGetTryAppInfo } from '@/service/use-try-app'
|
||||
import { ModelModeType, Resolution, TransferMethod } from '@/types/app'
|
||||
import { correctModelProvider, correctToolProvider } from '@/utils'
|
||||
import { userInputsFormToPromptVariables } from '@/utils/model-config'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { useTextGenerationCurrentProviderAndModelAndModelList } from '../../../header/account-setting/model-provider-page/hooks'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
}
|
||||
|
||||
const defaultModelConfig = {
|
||||
provider: 'langgenius/openai/openai',
|
||||
model_id: 'gpt-3.5-turbo',
|
||||
mode: ModelModeType.unset,
|
||||
configs: {
|
||||
prompt_template: '',
|
||||
prompt_variables: [] as PromptVariable[],
|
||||
},
|
||||
more_like_this: null,
|
||||
opening_statement: '',
|
||||
suggested_questions: [],
|
||||
sensitive_word_avoidance: null,
|
||||
speech_to_text: null,
|
||||
text_to_speech: null,
|
||||
file_upload: null,
|
||||
suggested_questions_after_answer: null,
|
||||
retriever_resource: null,
|
||||
annotation_reply: null,
|
||||
dataSets: [],
|
||||
agentConfig: DEFAULT_AGENT_SETTING,
|
||||
}
|
||||
const BasicAppPreview: FC<Props> = ({
|
||||
appId,
|
||||
}) => {
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
|
||||
const { data: appDetail, isLoading: isLoadingAppDetail } = useGetTryAppInfo(appId)
|
||||
const { data: collectionListFromServer, isLoading: isLoadingToolProviders } = useAllToolProviders()
|
||||
const collectionList = collectionListFromServer?.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
icon: basePath && typeof item.icon == 'string' && !item.icon.includes(basePath) ? `${basePath}${item.icon}` : item.icon,
|
||||
}
|
||||
})
|
||||
const datasetIds = (() => {
|
||||
if (isLoadingAppDetail)
|
||||
return []
|
||||
const modelConfig = appDetail?.model_config
|
||||
if (!modelConfig)
|
||||
return []
|
||||
let datasets: any = null
|
||||
|
||||
if (modelConfig.agent_mode?.tools?.find(({ dataset }: any) => dataset?.enabled))
|
||||
datasets = modelConfig.agent_mode?.tools.filter(({ dataset }: any) => dataset?.enabled)
|
||||
// new dataset struct
|
||||
else if (modelConfig.dataset_configs.datasets?.datasets?.length > 0)
|
||||
datasets = modelConfig.dataset_configs?.datasets?.datasets
|
||||
|
||||
if (datasets?.length && datasets?.length > 0)
|
||||
return datasets.map(({ dataset }: any) => dataset.id)
|
||||
|
||||
return []
|
||||
})()
|
||||
const { data: dataSetData, isLoading: isLoadingDatasets } = useGetTryAppDataSets(appId, datasetIds)
|
||||
const dataSets = dataSetData?.data || []
|
||||
const isLoading = isLoadingAppDetail || isLoadingDatasets || isLoadingToolProviders
|
||||
|
||||
const modelConfig: ModelConfig = ((modelConfig?: BackendModelConfig) => {
|
||||
if (isLoading || !modelConfig)
|
||||
return defaultModelConfig
|
||||
|
||||
const model = modelConfig.model
|
||||
|
||||
const newModelConfig = {
|
||||
provider: correctModelProvider(model.provider),
|
||||
model_id: model.name,
|
||||
mode: model.mode,
|
||||
configs: {
|
||||
prompt_template: modelConfig.pre_prompt || '',
|
||||
prompt_variables: userInputsFormToPromptVariables(
|
||||
[
|
||||
...(modelConfig.user_input_form as any),
|
||||
...(
|
||||
modelConfig.external_data_tools?.length
|
||||
? modelConfig.external_data_tools.map((item: any) => {
|
||||
return {
|
||||
external_data_tool: {
|
||||
variable: item.variable as string,
|
||||
label: item.label as string,
|
||||
enabled: item.enabled,
|
||||
type: item.type as string,
|
||||
config: item.config,
|
||||
required: true,
|
||||
icon: item.icon,
|
||||
icon_background: item.icon_background,
|
||||
},
|
||||
}
|
||||
})
|
||||
: []
|
||||
),
|
||||
],
|
||||
modelConfig.dataset_query_variable,
|
||||
),
|
||||
},
|
||||
more_like_this: modelConfig.more_like_this,
|
||||
opening_statement: modelConfig.opening_statement,
|
||||
suggested_questions: modelConfig.suggested_questions,
|
||||
sensitive_word_avoidance: modelConfig.sensitive_word_avoidance,
|
||||
speech_to_text: modelConfig.speech_to_text,
|
||||
text_to_speech: modelConfig.text_to_speech,
|
||||
file_upload: modelConfig.file_upload,
|
||||
suggested_questions_after_answer: modelConfig.suggested_questions_after_answer,
|
||||
retriever_resource: modelConfig.retriever_resource,
|
||||
annotation_reply: modelConfig.annotation_reply,
|
||||
external_data_tools: modelConfig.external_data_tools,
|
||||
dataSets,
|
||||
agentConfig: appDetail?.mode === 'agent-chat' ? {
|
||||
max_iteration: DEFAULT_AGENT_SETTING.max_iteration,
|
||||
...modelConfig.agent_mode,
|
||||
// remove dataset
|
||||
enabled: true, // modelConfig.agent_mode?.enabled is not correct. old app: the value of app with dataset's is always true
|
||||
tools: modelConfig.agent_mode?.tools.filter((tool: any) => {
|
||||
return !tool.dataset
|
||||
}).map((tool: any) => {
|
||||
const toolInCollectionList = collectionList?.find(c => tool.provider_id === c.id)
|
||||
return {
|
||||
...tool,
|
||||
isDeleted: appDetail?.deleted_tools?.some((deletedTool: any) => deletedTool.id === tool.id && deletedTool.tool_name === tool.tool_name),
|
||||
notAuthor: toolInCollectionList?.is_team_authorization === false,
|
||||
...(tool.provider_type === 'builtin'
|
||||
? {
|
||||
provider_id: correctToolProvider(tool.provider_name, !!toolInCollectionList),
|
||||
provider_name: correctToolProvider(tool.provider_name, !!toolInCollectionList),
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
}),
|
||||
} : DEFAULT_AGENT_SETTING,
|
||||
}
|
||||
return (newModelConfig as any)
|
||||
})(appDetail?.model_config)
|
||||
const mode = appDetail?.mode
|
||||
// const isChatApp = ['chat', 'advanced-chat', 'agent-chat'].includes(mode!)
|
||||
|
||||
// chat configuration
|
||||
const promptMode = modelConfig?.prompt_type === PromptMode.advanced ? PromptMode.advanced : PromptMode.simple
|
||||
const isAdvancedMode = promptMode === PromptMode.advanced
|
||||
const isAgent = mode === 'agent-chat'
|
||||
const chatPromptConfig = isAdvancedMode ? (modelConfig?.chat_prompt_config || clone(DEFAULT_CHAT_PROMPT_CONFIG)) : undefined
|
||||
const suggestedQuestions = modelConfig?.suggested_questions || []
|
||||
const moreLikeThisConfig = modelConfig?.more_like_this || { enabled: false }
|
||||
const suggestedQuestionsAfterAnswerConfig = modelConfig?.suggested_questions_after_answer || { enabled: false }
|
||||
const speechToTextConfig = modelConfig?.speech_to_text || { enabled: false }
|
||||
const textToSpeechConfig = modelConfig?.text_to_speech || { enabled: false, voice: '', language: '' }
|
||||
const citationConfig = modelConfig?.retriever_resource || { enabled: false }
|
||||
const annotationConfig = modelConfig?.annotation_reply || {
|
||||
id: '',
|
||||
enabled: false,
|
||||
score_threshold: ANNOTATION_DEFAULT.score_threshold,
|
||||
embedding_model: {
|
||||
embedding_provider_name: '',
|
||||
embedding_model_name: '',
|
||||
},
|
||||
}
|
||||
const moderationConfig = modelConfig?.sensitive_word_avoidance || { enabled: false }
|
||||
// completion configuration
|
||||
const completionPromptConfig = modelConfig?.completion_prompt_config || clone(DEFAULT_COMPLETION_PROMPT_CONFIG) as any
|
||||
|
||||
// prompt & model config
|
||||
const inputs = {}
|
||||
const query = ''
|
||||
const completionParams = useState<FormValue>({})
|
||||
|
||||
const {
|
||||
currentModel: currModel,
|
||||
} = useTextGenerationCurrentProviderAndModelAndModelList(
|
||||
{
|
||||
provider: modelConfig.provider,
|
||||
model: modelConfig.model_id,
|
||||
},
|
||||
)
|
||||
|
||||
const isShowVisionConfig = !!currModel?.features?.includes(ModelFeatureEnum.vision)
|
||||
const isShowDocumentConfig = !!currModel?.features?.includes(ModelFeatureEnum.document)
|
||||
const isShowAudioConfig = !!currModel?.features?.includes(ModelFeatureEnum.audio)
|
||||
const isAllowVideoUpload = !!currModel?.features?.includes(ModelFeatureEnum.video)
|
||||
const visionConfig = {
|
||||
enabled: false,
|
||||
number_limits: 2,
|
||||
detail: Resolution.low,
|
||||
transfer_methods: [TransferMethod.local_file],
|
||||
}
|
||||
|
||||
const featuresData: FeaturesData = useMemo(() => {
|
||||
return {
|
||||
moreLikeThis: modelConfig.more_like_this || { enabled: false },
|
||||
opening: {
|
||||
enabled: !!modelConfig.opening_statement,
|
||||
opening_statement: modelConfig.opening_statement || '',
|
||||
suggested_questions: modelConfig.suggested_questions || [],
|
||||
},
|
||||
moderation: modelConfig.sensitive_word_avoidance || { enabled: false },
|
||||
speech2text: modelConfig.speech_to_text || { enabled: false },
|
||||
text2speech: modelConfig.text_to_speech || { enabled: false },
|
||||
file: {
|
||||
image: {
|
||||
detail: modelConfig.file_upload?.image?.detail || Resolution.high,
|
||||
enabled: !!modelConfig.file_upload?.image?.enabled,
|
||||
number_limits: modelConfig.file_upload?.image?.number_limits || 3,
|
||||
transfer_methods: modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||
},
|
||||
enabled: !!(modelConfig.file_upload?.enabled || modelConfig.file_upload?.image?.enabled),
|
||||
allowed_file_types: modelConfig.file_upload?.allowed_file_types || [],
|
||||
allowed_file_extensions: modelConfig.file_upload?.allowed_file_extensions || [...FILE_EXTS[SupportUploadFileTypes.image], ...FILE_EXTS[SupportUploadFileTypes.video]].map(ext => `.${ext}`),
|
||||
allowed_file_upload_methods: modelConfig.file_upload?.allowed_file_upload_methods || modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||
number_limits: modelConfig.file_upload?.number_limits || modelConfig.file_upload?.image?.number_limits || 3,
|
||||
fileUploadConfig: {},
|
||||
} as FileUpload,
|
||||
suggested: modelConfig.suggested_questions_after_answer || { enabled: false },
|
||||
citation: modelConfig.retriever_resource || { enabled: false },
|
||||
annotationReply: modelConfig.annotation_reply || { enabled: false },
|
||||
}
|
||||
}, [modelConfig])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Loading type="area" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const value = {
|
||||
readonly: true,
|
||||
appId,
|
||||
isAPIKeySet: true,
|
||||
isTrailFinished: false,
|
||||
mode,
|
||||
modelModeType: '',
|
||||
promptMode,
|
||||
isAdvancedMode,
|
||||
isAgent,
|
||||
isOpenAI: false,
|
||||
isFunctionCall: false,
|
||||
collectionList: [],
|
||||
setPromptMode: noop,
|
||||
canReturnToSimpleMode: false,
|
||||
setCanReturnToSimpleMode: noop,
|
||||
chatPromptConfig,
|
||||
completionPromptConfig,
|
||||
currentAdvancedPrompt: '',
|
||||
setCurrentAdvancedPrompt: noop,
|
||||
conversationHistoriesRole: completionPromptConfig.conversation_histories_role,
|
||||
showHistoryModal: false,
|
||||
setConversationHistoriesRole: noop,
|
||||
hasSetBlockStatus: true,
|
||||
conversationId: '',
|
||||
introduction: '',
|
||||
setIntroduction: noop,
|
||||
suggestedQuestions,
|
||||
setSuggestedQuestions: noop,
|
||||
setConversationId: noop,
|
||||
controlClearChatMessage: false,
|
||||
setControlClearChatMessage: noop,
|
||||
prevPromptConfig: {},
|
||||
setPrevPromptConfig: noop,
|
||||
moreLikeThisConfig,
|
||||
setMoreLikeThisConfig: noop,
|
||||
suggestedQuestionsAfterAnswerConfig,
|
||||
setSuggestedQuestionsAfterAnswerConfig: noop,
|
||||
speechToTextConfig,
|
||||
setSpeechToTextConfig: noop,
|
||||
textToSpeechConfig,
|
||||
setTextToSpeechConfig: noop,
|
||||
citationConfig,
|
||||
setCitationConfig: noop,
|
||||
annotationConfig,
|
||||
setAnnotationConfig: noop,
|
||||
moderationConfig,
|
||||
setModerationConfig: noop,
|
||||
externalDataToolsConfig: {},
|
||||
setExternalDataToolsConfig: noop,
|
||||
formattingChanged: false,
|
||||
setFormattingChanged: noop,
|
||||
inputs,
|
||||
setInputs: noop,
|
||||
query,
|
||||
setQuery: noop,
|
||||
completionParams,
|
||||
setCompletionParams: noop,
|
||||
modelConfig,
|
||||
setModelConfig: noop,
|
||||
showSelectDataSet: noop,
|
||||
dataSets,
|
||||
setDataSets: noop,
|
||||
datasetConfigs: [],
|
||||
datasetConfigsRef: {},
|
||||
setDatasetConfigs: noop,
|
||||
hasSetContextVar: true,
|
||||
isShowVisionConfig,
|
||||
visionConfig,
|
||||
setVisionConfig: noop,
|
||||
isAllowVideoUpload,
|
||||
isShowDocumentConfig,
|
||||
isShowAudioConfig,
|
||||
rerankSettingModalOpen: false,
|
||||
setRerankSettingModalOpen: noop,
|
||||
}
|
||||
return (
|
||||
<ConfigContext.Provider value={value as any}>
|
||||
<FeaturesProvider features={featuresData}>
|
||||
<div className="flex h-full w-full flex-col bg-components-panel-on-panel-item-bg">
|
||||
<div className="relative flex h-[200px] grow">
|
||||
<div className="flex h-full w-full shrink-0 flex-col sm:w-1/2">
|
||||
<Config />
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<div className="relative flex h-full w-1/2 grow flex-col overflow-y-auto " style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
|
||||
<div className="flex grow flex-col rounded-tl-2xl border-l-[0.5px] border-t-[0.5px] border-components-panel-border bg-chatbot-bg ">
|
||||
<Debug
|
||||
isAPIKeySet
|
||||
onSetting={noop}
|
||||
inputs={inputs}
|
||||
modelParameterParams={{
|
||||
setModel: noop,
|
||||
onCompletionParamsChange: noop,
|
||||
}}
|
||||
debugWithMultipleModel={false}
|
||||
multipleModelConfigs={[]}
|
||||
onMultipleModelConfigsChange={noop}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</FeaturesProvider>
|
||||
</ConfigContext.Provider>
|
||||
)
|
||||
}
|
||||
export default React.memo(BasicAppPreview)
|
||||
@@ -1,39 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import * as React from 'react'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import WorkflowPreview from '@/app/components/workflow/workflow-preview'
|
||||
import { useGetTryAppFlowPreview } from '@/service/use-try-app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const FlowAppPreview: FC<Props> = ({
|
||||
appId,
|
||||
className,
|
||||
}) => {
|
||||
const { data, isLoading } = useGetTryAppFlowPreview(appId)
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Loading type="area" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (!data)
|
||||
return null
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<WorkflowPreview
|
||||
{...data.graph}
|
||||
className={cn(className)}
|
||||
miniMapToRight
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(FlowAppPreview)
|
||||
@@ -1,25 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { TryAppInfo } from '@/service/try-app'
|
||||
import * as React from 'react'
|
||||
import BasicAppPreview from './basic-app-preview'
|
||||
import FlowAppPreview from './flow-app-preview'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
appDetail: TryAppInfo
|
||||
}
|
||||
|
||||
const Preview: FC<Props> = ({
|
||||
appId,
|
||||
appDetail,
|
||||
}) => {
|
||||
const isBasicApp = ['agent-chat', 'chat', 'completion'].includes(appDetail.mode)
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
{isBasicApp ? <BasicAppPreview appId={appId} /> : <FlowAppPreview appId={appId} className="h-full" />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Preview)
|
||||
@@ -1,37 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import TabHeader from '../../base/tab-header'
|
||||
|
||||
export enum TypeEnum {
|
||||
TRY = 'try',
|
||||
DETAIL = 'detail',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
value: TypeEnum
|
||||
onChange: (value: TypeEnum) => void
|
||||
}
|
||||
|
||||
const Tab: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const tabs = [
|
||||
{ id: TypeEnum.TRY, name: t('tryApp.tabHeader.try', { ns: 'explore' }) },
|
||||
{ id: TypeEnum.DETAIL, name: t('tryApp.tabHeader.detail', { ns: 'explore' }) },
|
||||
]
|
||||
return (
|
||||
<TabHeader
|
||||
items={tabs}
|
||||
value={value}
|
||||
onChange={onChange as (value: string) => void}
|
||||
itemClassName="ml-0 system-md-semibold-uppercase"
|
||||
itemWrapClassName="pt-2"
|
||||
activeItemClassName="border-util-colors-blue-brand-blue-brand-500"
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(Tab)
|
||||
@@ -34,7 +34,7 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { changeLanguage } from '@/i18n-config/client'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { AppSourceType, fetchSavedMessage as doFetchSavedMessage, removeMessage, saveMessage } from '@/service/share'
|
||||
import { fetchSavedMessage as doFetchSavedMessage, removeMessage, saveMessage } from '@/service/share'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { userInputsFormToPromptVariables } from '@/utils/model-config'
|
||||
@@ -73,7 +73,6 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
isWorkflow = false,
|
||||
}) => {
|
||||
const { notify } = Toast
|
||||
const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp
|
||||
|
||||
const { t } = useTranslation()
|
||||
const media = useBreakpoints()
|
||||
@@ -103,18 +102,16 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
// save message
|
||||
const [savedMessages, setSavedMessages] = useState<SavedMessage[]>([])
|
||||
const fetchSavedMessage = useCallback(async () => {
|
||||
if (!appId)
|
||||
return
|
||||
const res: any = await doFetchSavedMessage(appSourceType, appId)
|
||||
const res: any = await doFetchSavedMessage(isInstalledApp, appId)
|
||||
setSavedMessages(res.data)
|
||||
}, [appSourceType, appId])
|
||||
}, [isInstalledApp, appId])
|
||||
const handleSaveMessage = async (messageId: string) => {
|
||||
await saveMessage(messageId, appSourceType, appId)
|
||||
await saveMessage(messageId, isInstalledApp, appId)
|
||||
notify({ type: 'success', message: t('api.saved', { ns: 'common' }) })
|
||||
fetchSavedMessage()
|
||||
}
|
||||
const handleRemoveSavedMessage = async (messageId: string) => {
|
||||
await removeMessage(messageId, appSourceType, appId)
|
||||
await removeMessage(messageId, isInstalledApp, appId)
|
||||
notify({ type: 'success', message: t('api.remove', { ns: 'common' }) })
|
||||
fetchSavedMessage()
|
||||
}
|
||||
@@ -427,8 +424,9 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
isCallBatchAPI={isCallBatchAPI}
|
||||
isPC={isPC}
|
||||
isMobile={!isPC}
|
||||
appSourceType={isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp}
|
||||
isInstalledApp={isInstalledApp}
|
||||
appId={appId}
|
||||
installedAppInfo={installedAppInfo}
|
||||
isError={task?.status === TaskStatus.failed}
|
||||
promptConfig={promptConfig}
|
||||
moreLikeThisEnabled={!!moreLikeThisConfig?.enabled}
|
||||
|
||||
@@ -4,8 +4,8 @@ import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
||||
import type { WorkflowProcess } from '@/app/components/base/chat/types'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import type { PromptConfig } from '@/models/debug'
|
||||
import type { InstalledApp } from '@/models/explore'
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import type { AppSourceType } from '@/service/share'
|
||||
import type { VisionFile, VisionSettings } from '@/types/app'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
@@ -35,8 +35,9 @@ export type IResultProps = {
|
||||
isCallBatchAPI: boolean
|
||||
isPC: boolean
|
||||
isMobile: boolean
|
||||
appSourceType: AppSourceType
|
||||
appId?: string
|
||||
isInstalledApp: boolean
|
||||
appId: string
|
||||
installedAppInfo?: InstalledApp
|
||||
isError: boolean
|
||||
isShowTextToSpeech: boolean
|
||||
promptConfig: PromptConfig | null
|
||||
@@ -62,8 +63,9 @@ const Result: FC<IResultProps> = ({
|
||||
isCallBatchAPI,
|
||||
isPC,
|
||||
isMobile,
|
||||
appSourceType,
|
||||
isInstalledApp,
|
||||
appId,
|
||||
installedAppInfo,
|
||||
isError,
|
||||
isShowTextToSpeech,
|
||||
promptConfig,
|
||||
@@ -131,7 +133,7 @@ const Result: FC<IResultProps> = ({
|
||||
})
|
||||
|
||||
const handleFeedback = async (feedback: FeedbackType) => {
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, appSourceType, appId)
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, isInstalledApp, installedAppInfo?.id)
|
||||
setFeedback(feedback)
|
||||
}
|
||||
|
||||
@@ -145,9 +147,9 @@ const Result: FC<IResultProps> = ({
|
||||
setIsStopping(true)
|
||||
try {
|
||||
if (isWorkflow)
|
||||
await stopWorkflowMessage(appId!, currentTaskId, appSourceType, appId || '')
|
||||
await stopWorkflowMessage(appId, currentTaskId, isInstalledApp, installedAppInfo?.id || '')
|
||||
else
|
||||
await stopChatMessageResponding(appId!, currentTaskId, appSourceType, appId || '')
|
||||
await stopChatMessageResponding(appId, currentTaskId, isInstalledApp, installedAppInfo?.id || '')
|
||||
abortControllerRef.current?.abort()
|
||||
}
|
||||
catch (error) {
|
||||
@@ -157,7 +159,7 @@ const Result: FC<IResultProps> = ({
|
||||
finally {
|
||||
setIsStopping(false)
|
||||
}
|
||||
}, [appId, currentTaskId, appSourceType, appId, isStopping, isWorkflow, notify])
|
||||
}, [appId, currentTaskId, installedAppInfo?.id, isInstalledApp, isStopping, isWorkflow, notify])
|
||||
|
||||
useEffect(() => {
|
||||
if (!onRunControlChange)
|
||||
@@ -466,8 +468,8 @@ const Result: FC<IResultProps> = ({
|
||||
}))
|
||||
},
|
||||
},
|
||||
appSourceType,
|
||||
appId,
|
||||
isInstalledApp,
|
||||
installedAppInfo?.id,
|
||||
).catch((error) => {
|
||||
setRespondingFalse()
|
||||
resetRunState()
|
||||
@@ -512,7 +514,7 @@ const Result: FC<IResultProps> = ({
|
||||
getAbortController: (abortController) => {
|
||||
abortControllerRef.current = abortController
|
||||
},
|
||||
}, appSourceType, appId)
|
||||
}, isInstalledApp, installedAppInfo?.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,8 +562,8 @@ const Result: FC<IResultProps> = ({
|
||||
feedback={feedback}
|
||||
onSave={handleSaveMessage}
|
||||
isMobile={isMobile}
|
||||
appSourceType={appSourceType}
|
||||
installedAppId={appId}
|
||||
isInstalledApp={isInstalledApp}
|
||||
installedAppId={installedAppInfo?.id}
|
||||
isLoading={isCallBatchAPI ? (!completionRes && isResponding) : false}
|
||||
taskId={isCallBatchAPI ? ((taskId as number) < 10 ? `0${taskId}` : `${taskId}`) : undefined}
|
||||
controlClearMoreLikeThis={controlClearMoreLikeThis}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
type TaskParam = {
|
||||
inputs: Record<string, any>
|
||||
}
|
||||
|
||||
export type Task = {
|
||||
id: number
|
||||
status: TaskStatus
|
||||
params: TaskParam
|
||||
}
|
||||
|
||||
export enum TaskStatus {
|
||||
pending = 'pending',
|
||||
running = 'running',
|
||||
completed = 'completed',
|
||||
failed = 'failed',
|
||||
}
|
||||
@@ -10,7 +10,6 @@ type Props = {
|
||||
value: boolean
|
||||
required?: boolean
|
||||
onChange: (value: boolean) => void
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
const BoolInput: FC<Props> = ({
|
||||
@@ -18,7 +17,6 @@ const BoolInput: FC<Props> = ({
|
||||
onChange,
|
||||
name,
|
||||
required,
|
||||
readonly,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const handleChange = useCallback(() => {
|
||||
@@ -30,7 +28,6 @@ const BoolInput: FC<Props> = ({
|
||||
className="!h-4 !w-4"
|
||||
checked={!!value}
|
||||
onCheck={handleChange}
|
||||
disabled={readonly}
|
||||
/>
|
||||
<div className="system-sm-medium flex items-center gap-1 text-text-secondary">
|
||||
{name}
|
||||
|
||||
@@ -59,12 +59,12 @@ const InputItem: FC<Props> = ({
|
||||
}, [onRemove])
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'hover:cursor-text hover:bg-state-base-hover', 'relative flex !h-[30px] items-center')}>
|
||||
<div className={cn(className, 'hover:cursor-text hover:bg-state-base-hover', 'relative flex h-full')}>
|
||||
{(!readOnly)
|
||||
? (
|
||||
<Input
|
||||
instanceId={instanceId}
|
||||
className={cn(isFocus ? 'bg-components-input-bg-active' : 'bg-width', 'h-full w-0 grow px-3 py-1')}
|
||||
className={cn(isFocus ? 'bg-components-input-bg-active' : 'bg-width', 'w-0 grow px-3 py-1')}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
readOnly={readOnly}
|
||||
@@ -79,13 +79,13 @@ const InputItem: FC<Props> = ({
|
||||
)
|
||||
: (
|
||||
<div
|
||||
className="h-full w-full pl-0.5 leading-[18px]"
|
||||
className="h-[18px] w-full pl-0.5 leading-[18px]"
|
||||
>
|
||||
{!hasValue && <div className="text-xs font-normal text-text-quaternary">{placeholder}</div>}
|
||||
{hasValue && (
|
||||
<Input
|
||||
instanceId={instanceId}
|
||||
className={cn(isFocus ? 'border-components-input-border-active bg-components-input-bg-active shadow-xs' : 'border-components-input-border-hover bg-components-input-bg-normal', 'h-full w-0 grow rounded-lg border px-3 py-[6px]')}
|
||||
className={cn(isFocus ? 'border-components-input-border-active bg-components-input-bg-active shadow-xs' : 'border-components-input-border-hover bg-components-input-bg-normal', 'w-0 grow rounded-lg border px-3 py-[6px]')}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
readOnly={readOnly}
|
||||
|
||||
@@ -61,14 +61,12 @@ type WorkflowPreviewProps = {
|
||||
edges: Edge[]
|
||||
viewport: Viewport
|
||||
className?: string
|
||||
miniMapToRight?: boolean
|
||||
}
|
||||
const WorkflowPreview = ({
|
||||
nodes,
|
||||
edges,
|
||||
viewport,
|
||||
className,
|
||||
miniMapToRight,
|
||||
}: WorkflowPreviewProps) => {
|
||||
const [nodesData, setNodesData] = useState(() => initialNodes(nodes, edges))
|
||||
const [edgesData, setEdgesData] = useState(() => initialEdges(edges, nodes))
|
||||
@@ -99,7 +97,8 @@ const WorkflowPreview = ({
|
||||
height: 72,
|
||||
}}
|
||||
maskColor="var(--color-workflow-minimap-bg)"
|
||||
className={cn('!absolute !bottom-14 z-[9] !m-0 !h-[72px] !w-[102px] !rounded-lg !border-[0.5px] !border-divider-subtle !bg-background-default-subtle !shadow-md !shadow-shadow-shadow-5', miniMapToRight ? '!right-4' : '!left-4')}
|
||||
className="!absolute !bottom-14 !left-4 z-[9] !m-0 !h-[72px] !w-[102px] !rounded-lg !border-[0.5px]
|
||||
!border-divider-subtle !bg-background-default-subtle !shadow-md !shadow-shadow-shadow-5"
|
||||
/>
|
||||
<div className="absolute bottom-4 left-4 z-[9] mt-1 flex items-center gap-2">
|
||||
<ZoomInOut />
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { CurrentTryAppParams } from './explore-context'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { createContext } from 'use-context-selector'
|
||||
|
||||
type Props = {
|
||||
currentApp?: CurrentTryAppParams
|
||||
isShowTryAppPanel: boolean
|
||||
setShowTryAppPanel: (showTryAppPanel: boolean, params?: CurrentTryAppParams) => void
|
||||
controlHideCreateFromTemplatePanel: number
|
||||
}
|
||||
|
||||
const AppListContext = createContext<Props>({
|
||||
isShowTryAppPanel: false,
|
||||
setShowTryAppPanel: noop,
|
||||
currentApp: undefined,
|
||||
controlHideCreateFromTemplatePanel: 0,
|
||||
})
|
||||
|
||||
export default AppListContext
|
||||
@@ -29,7 +29,6 @@ import { PromptMode } from '@/models/debug'
|
||||
import { AppModeEnum, ModelModeType, Resolution, RETRIEVE_TYPE, TransferMethod } from '@/types/app'
|
||||
|
||||
type IDebugConfiguration = {
|
||||
readonly?: boolean
|
||||
appId: string
|
||||
isAPIKeySet: boolean
|
||||
isTrailFinished: boolean
|
||||
@@ -109,7 +108,6 @@ type IDebugConfiguration = {
|
||||
}
|
||||
|
||||
const DebugConfigurationContext = createContext<IDebugConfiguration>({
|
||||
readonly: false,
|
||||
appId: '',
|
||||
isAPIKeySet: false,
|
||||
isTrailFinished: false,
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import type { App, InstalledApp } from '@/models/explore'
|
||||
import type { InstalledApp } from '@/models/explore'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { createContext } from 'use-context-selector'
|
||||
|
||||
export type CurrentTryAppParams = {
|
||||
appId: string
|
||||
app: App
|
||||
}
|
||||
|
||||
type IExplore = {
|
||||
controlUpdateInstalledApps: number
|
||||
setControlUpdateInstalledApps: (controlUpdateInstalledApps: number) => void
|
||||
@@ -15,9 +10,6 @@ type IExplore = {
|
||||
setInstalledApps: (installedApps: InstalledApp[]) => void
|
||||
isFetchingInstalledApps: boolean
|
||||
setIsFetchingInstalledApps: (isFetchingInstalledApps: boolean) => void
|
||||
currentApp?: CurrentTryAppParams
|
||||
isShowTryAppPanel: boolean
|
||||
setShowTryAppPanel: (showTryAppPanel: boolean, params?: CurrentTryAppParams) => void
|
||||
}
|
||||
|
||||
const ExploreContext = createContext<IExplore>({
|
||||
@@ -28,9 +20,6 @@ const ExploreContext = createContext<IExplore>({
|
||||
setInstalledApps: noop,
|
||||
isFetchingInstalledApps: false,
|
||||
setIsFetchingInstalledApps: noop,
|
||||
isShowTryAppPanel: false,
|
||||
setShowTryAppPanel: noop,
|
||||
currentApp: undefined,
|
||||
})
|
||||
|
||||
export default ExploreContext
|
||||
|
||||
@@ -178,7 +178,7 @@ export default antfu(
|
||||
rules: {
|
||||
// 'dify-i18n/no-as-any-in-t': ['error', { mode: 'all' }],
|
||||
'dify-i18n/no-as-any-in-t': 'error',
|
||||
'dify-i18n/no-legacy-namespace-prefix': 'error',
|
||||
// 'dify-i18n/no-legacy-namespace-prefix': 'error',
|
||||
// 'dify-i18n/require-ns-option': 'error',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,369 +0,0 @@
|
||||
{
|
||||
"de-DE": {
|
||||
"explore": {
|
||||
"added": {},
|
||||
"modified": {},
|
||||
"deleted": [
|
||||
"sidebar.discovery",
|
||||
"sidebar.workspace",
|
||||
"apps.title",
|
||||
"apps.description",
|
||||
"apps.allCategories",
|
||||
"appCard.addToWorkspace"
|
||||
]
|
||||
}
|
||||
},
|
||||
"en-US": {
|
||||
"common": {
|
||||
"added": {
|
||||
"chat.inputDisabledPlaceholder": "Preview Only"
|
||||
},
|
||||
"modified": {},
|
||||
"deleted": []
|
||||
},
|
||||
"explore": {
|
||||
"added": {
|
||||
"sidebar.title": "App gallery",
|
||||
"sidebar.webApps": "Web apps",
|
||||
"sidebar.noApps.title": "No web apps",
|
||||
"sidebar.noApps.description": "Published web apps will appear here",
|
||||
"sidebar.noApps.learnMore": "Learn more",
|
||||
"apps.resultNum": "{{num}} results",
|
||||
"apps.resetFilter": "Clear filter",
|
||||
"appCard.try": "Details",
|
||||
"tryApp.tabHeader.try": "Try it",
|
||||
"tryApp.tabHeader.detail": "Orchestration Details",
|
||||
"tryApp.createFromSampleApp": "Create from this sample app",
|
||||
"tryApp.category": "Category",
|
||||
"tryApp.requirements": "Requirements",
|
||||
"tryApp.tryInfo": "This is a sample app. You can try up to 5 messages. To keep using it, click \"Create form this sample app\" and set it up!",
|
||||
"banner.viewMore": "VIEW MORE"
|
||||
},
|
||||
"modified": {
|
||||
"apps.title": "Try Dify's curated apps to find AI solutions for your business",
|
||||
"apps.allCategories": "All",
|
||||
"appCard.addToWorkspace": "Use template"
|
||||
},
|
||||
"deleted": [
|
||||
"sidebar.discovery",
|
||||
"sidebar.workspace",
|
||||
"apps.description",
|
||||
"appCard.customize"
|
||||
]
|
||||
}
|
||||
},
|
||||
"es-ES": {
|
||||
"explore": {
|
||||
"added": {},
|
||||
"modified": {},
|
||||
"deleted": [
|
||||
"sidebar.discovery",
|
||||
"sidebar.workspace",
|
||||
"apps.title",
|
||||
"apps.description",
|
||||
"apps.allCategories",
|
||||
"appCard.addToWorkspace"
|
||||
]
|
||||
}
|
||||
},
|
||||
"fa-IR": {
|
||||
"explore": {
|
||||
"added": {},
|
||||
"modified": {},
|
||||
"deleted": [
|
||||
"sidebar.discovery",
|
||||
"sidebar.workspace",
|
||||
"apps.title",
|
||||
"apps.description",
|
||||
"apps.allCategories",
|
||||
"appCard.addToWorkspace"
|
||||
]
|
||||
}
|
||||
},
|
||||
"fr-FR": {
|
||||
"explore": {
|
||||
"added": {},
|
||||
"modified": {},
|
||||
"deleted": [
|
||||
"sidebar.discovery",
|
||||
"sidebar.workspace",
|
||||
"apps.title",
|
||||
"apps.description",
|
||||
"apps.allCategories",
|
||||
"appCard.addToWorkspace"
|
||||
]
|
||||
}
|
||||
},
|
||||
"hi-IN": {
|
||||
"explore": {
|
||||
"added": {},
|
||||
"modified": {},
|
||||
"deleted": [
|
||||
"sidebar.discovery",
|
||||
"sidebar.workspace",
|
||||
"apps.title",
|
||||
"apps.description",
|
||||
"apps.allCategories",
|
||||
"appCard.addToWorkspace"
|
||||
]
|
||||
}
|
||||
},
|
||||
"id-ID": {
|
||||
"explore": {
|
||||
"added": {},
|
||||
"modified": {},
|
||||
"deleted": [
|
||||
"sidebar.workspace",
|
||||
"sidebar.discovery",
|
||||
"apps.allCategories",
|
||||
"apps.description",
|
||||
"apps.title",
|
||||
"appCard.addToWorkspace"
|
||||
]
|
||||
}
|
||||
},
|
||||
"it-IT": {
|
||||
"explore": {
|
||||
"added": {},
|
||||
"modified": {},
|
||||
"deleted": [
|
||||
"sidebar.discovery",
|
||||
"sidebar.workspace",
|
||||
"apps.title",
|
||||
"apps.description",
|
||||
"apps.allCategories",
|
||||
"appCard.addToWorkspace"
|
||||
]
|
||||
}
|
||||
},
|
||||
"ja-JP": {
|
||||
"common": {
|
||||
"added": {
|
||||
"chat.inputDisabledPlaceholder": "プレビューのみ"
|
||||
},
|
||||
"modified": {},
|
||||
"deleted": []
|
||||
},
|
||||
"explore": {
|
||||
"added": {
|
||||
"sidebar.title": "アプリギャラリー",
|
||||
"sidebar.webApps": "Webアプリ",
|
||||
"sidebar.noApps.title": "Webアプリなし",
|
||||
"sidebar.noApps.description": "公開されたWebアプリがここに表示されます",
|
||||
"sidebar.noApps.learnMore": "詳細",
|
||||
"apps.resultNum": "{{num}}件の結果",
|
||||
"apps.resetFilter": "クリア",
|
||||
"appCard.try": "詳細",
|
||||
"tryApp.tabHeader.try": "お試し",
|
||||
"tryApp.tabHeader.detail": "オーケストレーション詳細",
|
||||
"tryApp.createFromSampleApp": "テンプレートから作成",
|
||||
"tryApp.category": "カテゴリー",
|
||||
"tryApp.requirements": "必要項目",
|
||||
"tryApp.tryInfo": "これはサンプルアプリです。最大5件のメッセージまでお試しいただけます。引き続き利用するには、「テンプレートから作成」 をクリックして設定を行ってください。",
|
||||
"banner.viewMore": "もっと見る"
|
||||
},
|
||||
"modified": {
|
||||
"apps.title": "Difyの厳選アプリを試して、ビジネス向けのAIソリューションを見つけましょう",
|
||||
"apps.allCategories": "全て",
|
||||
"appCard.addToWorkspace": "テンプレートを使用"
|
||||
},
|
||||
"deleted": [
|
||||
"sidebar.discovery",
|
||||
"sidebar.workspace",
|
||||
"apps.description"
|
||||
]
|
||||
}
|
||||
},
|
||||
"ko-KR": {
|
||||
"explore": {
|
||||
"added": {},
|
||||
"modified": {},
|
||||
"deleted": [
|
||||
"sidebar.discovery",
|
||||
"sidebar.workspace",
|
||||
"apps.title",
|
||||
"apps.description",
|
||||
"apps.allCategories",
|
||||
"appCard.addToWorkspace"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pl-PL": {
|
||||
"explore": {
|
||||
"added": {},
|
||||
"modified": {},
|
||||
"deleted": [
|
||||
"sidebar.discovery",
|
||||
"sidebar.workspace",
|
||||
"apps.title",
|
||||
"apps.description",
|
||||
"apps.allCategories",
|
||||
"appCard.addToWorkspace"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pt-BR": {
|
||||
"explore": {
|
||||
"added": {},
|
||||
"modified": {},
|
||||
"deleted": [
|
||||
"sidebar.discovery",
|
||||
"sidebar.workspace",
|
||||
"apps.title",
|
||||
"apps.description",
|
||||
"apps.allCategories",
|
||||
"appCard.addToWorkspace"
|
||||
]
|
||||
}
|
||||
},
|
||||
"ro-RO": {
|
||||
"explore": {
|
||||
"added": {},
|
||||
"modified": {},
|
||||
"deleted": [
|
||||
"sidebar.discovery",
|
||||
"sidebar.workspace",
|
||||
"apps.title",
|
||||
"apps.description",
|
||||
"apps.allCategories",
|
||||
"appCard.addToWorkspace"
|
||||
]
|
||||
}
|
||||
},
|
||||
"ru-RU": {
|
||||
"explore": {
|
||||
"added": {},
|
||||
"modified": {},
|
||||
"deleted": [
|
||||
"sidebar.discovery",
|
||||
"sidebar.workspace",
|
||||
"apps.title",
|
||||
"apps.description",
|
||||
"apps.allCategories",
|
||||
"appCard.addToWorkspace"
|
||||
]
|
||||
}
|
||||
},
|
||||
"sl-SI": {
|
||||
"explore": {
|
||||
"added": {},
|
||||
"modified": {},
|
||||
"deleted": [
|
||||
"sidebar.discovery",
|
||||
"sidebar.workspace",
|
||||
"apps.title",
|
||||
"apps.description",
|
||||
"apps.allCategories",
|
||||
"appCard.addToWorkspace"
|
||||
]
|
||||
}
|
||||
},
|
||||
"th-TH": {
|
||||
"explore": {
|
||||
"added": {},
|
||||
"modified": {},
|
||||
"deleted": [
|
||||
"sidebar.discovery",
|
||||
"sidebar.workspace",
|
||||
"apps.title",
|
||||
"apps.description",
|
||||
"apps.allCategories",
|
||||
"appCard.addToWorkspace"
|
||||
]
|
||||
}
|
||||
},
|
||||
"tr-TR": {
|
||||
"explore": {
|
||||
"added": {},
|
||||
"modified": {},
|
||||
"deleted": [
|
||||
"sidebar.discovery",
|
||||
"sidebar.workspace",
|
||||
"apps.title",
|
||||
"apps.description",
|
||||
"apps.allCategories",
|
||||
"appCard.addToWorkspace"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uk-UA": {
|
||||
"explore": {
|
||||
"added": {},
|
||||
"modified": {},
|
||||
"deleted": [
|
||||
"sidebar.discovery",
|
||||
"sidebar.workspace",
|
||||
"apps.title",
|
||||
"apps.description",
|
||||
"apps.allCategories",
|
||||
"appCard.addToWorkspace"
|
||||
]
|
||||
}
|
||||
},
|
||||
"vi-VN": {
|
||||
"explore": {
|
||||
"added": {},
|
||||
"modified": {},
|
||||
"deleted": [
|
||||
"sidebar.discovery",
|
||||
"sidebar.workspace",
|
||||
"apps.title",
|
||||
"apps.description",
|
||||
"apps.allCategories",
|
||||
"appCard.addToWorkspace"
|
||||
]
|
||||
}
|
||||
},
|
||||
"zh-Hans": {
|
||||
"common": {
|
||||
"added": {
|
||||
"chat.inputDisabledPlaceholder": "仅供试用"
|
||||
},
|
||||
"modified": {},
|
||||
"deleted": []
|
||||
},
|
||||
"explore": {
|
||||
"added": {
|
||||
"sidebar.title": "应用库",
|
||||
"sidebar.webApps": "WEB APPS",
|
||||
"sidebar.noApps.title": "没有 web apps",
|
||||
"sidebar.noApps.description": "已发布的 web apps 将出现在此处",
|
||||
"sidebar.noApps.learnMore": "了解更多",
|
||||
"apps.resultNum": "{{num}} 个结果",
|
||||
"apps.resetFilter": "清除筛选",
|
||||
"appCard.try": "详情",
|
||||
"tryApp.tabHeader.try": "试用",
|
||||
"tryApp.tabHeader.detail": "编排详情",
|
||||
"tryApp.createFromSampleApp": "从此模板创建应用",
|
||||
"tryApp.category": "分类",
|
||||
"tryApp.requirements": "必须配置项",
|
||||
"tryApp.tryInfo": "这是一个示例应用,您可以试用最多 5 条消息。如需继续使用,请点击 “从此模板创建应用” 并完成配置!",
|
||||
"banner.viewMore": "查看更多"
|
||||
},
|
||||
"modified": {
|
||||
"apps.title": "试用 Dify 精选示例应用,为您的业务寻找 AI 解决方案",
|
||||
"apps.allCategories": "所有",
|
||||
"appCard.addToWorkspace": "使用模板"
|
||||
},
|
||||
"deleted": [
|
||||
"sidebar.discovery",
|
||||
"sidebar.workspace",
|
||||
"apps.description"
|
||||
]
|
||||
}
|
||||
},
|
||||
"zh-Hant": {
|
||||
"explore": {
|
||||
"added": {},
|
||||
"modified": {},
|
||||
"deleted": [
|
||||
"sidebar.discovery",
|
||||
"sidebar.workspace",
|
||||
"apps.title",
|
||||
"apps.description",
|
||||
"apps.allCategories",
|
||||
"appCard.addToWorkspace"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,32 +158,10 @@ We have a list of languages that we support in the `languages.ts` file. But some
|
||||
|
||||
## Utility scripts
|
||||
|
||||
- Auto-fill translations: `pnpm run i18n:gen --file app common --lang zh-Hans ja-JP [--dry-run]`
|
||||
- Use space-separated values; repeat `--file` / `--lang` as needed. Defaults to all en-US files and all supported locales except en-US.
|
||||
- Protects placeholders (`{{var}}`, `${var}`, `<tag>`) before translation and restores them after.
|
||||
- Check missing/extra keys: `pnpm run i18n:check --file app billing --lang zh-Hans [--auto-remove]`
|
||||
- Use space-separated values; repeat `--file` / `--lang` as needed. Returns non-zero on missing/extra keys; `--auto-remove` deletes extra keys automatically.
|
||||
|
||||
## Automatic Translation
|
||||
|
||||
Translation is handled automatically by Claude Code GitHub Actions. When changes are pushed to `web/i18n/en-US/*.json` on the main branch:
|
||||
|
||||
1. Claude Code analyzes the git diff to detect changes
|
||||
1. Identifies three types of changes:
|
||||
- **ADD**: New keys that need translation
|
||||
- **UPDATE**: Modified keys that need re-translation (even if target language has existing translation)
|
||||
- **DELETE**: Removed keys that need to be deleted from other languages
|
||||
1. Runs `i18n:check` to verify the initial sync status.
|
||||
1. Translates missing/updated keys while preserving placeholders (`{{var}}`, `${var}`, `<tag>`) and removes deleted keys.
|
||||
1. Runs `lint:fix` to sort JSON keys and `i18n:check` again to ensure everything is synchronized.
|
||||
1. Creates a PR with the translations.
|
||||
|
||||
### Manual Trigger
|
||||
|
||||
To manually trigger translation:
|
||||
|
||||
1. Go to Actions > "Translate i18n Files with Claude Code"
|
||||
1. Click "Run workflow"
|
||||
1. Optionally configure:
|
||||
- **files**: Specific files to translate (space-separated, e.g., "app common")
|
||||
- **languages**: Specific languages to translate (space-separated, e.g., "zh-Hans ja-JP")
|
||||
- **mode**: `incremental` (default, only changes) or `full` (check all keys)
|
||||
|
||||
Workflow: `.github/workflows/translate-i18n-claude.yml`
|
||||
Workflows: `.github/workflows/translate-i18n-base-on-english.yml` auto-runs the translation generator on `web/i18n/en-US/*.json` changes to main. `i18n:check` is a manual script (not run in CI).
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"appCard.addToWorkspace": "إضافة إلى مساحة العمل",
|
||||
"appCard.customize": "تخصيص",
|
||||
"appCustomize.nameRequired": "اسم التطبيق مطلوب",
|
||||
"appCustomize.subTitle": "أيقونة التطبيق واسمه",
|
||||
"appCustomize.title": "إنشاء تطبيق من {{name}}",
|
||||
"apps.allCategories": "موصى به",
|
||||
"apps.description": "استخدم تطبيقات القوالب هذه فورًا أو خصص تطبيقاتك الخاصة بناءً على القوالب.",
|
||||
"apps.title": "استكشاف التطبيقات",
|
||||
"category.Agent": "وكيل",
|
||||
"category.Assistant": "مساعد",
|
||||
@@ -21,5 +23,7 @@
|
||||
"sidebar.chat": "دردشة",
|
||||
"sidebar.delete.content": "هل أنت متأكد أنك تريد حذف هذا التطبيق؟",
|
||||
"sidebar.delete.title": "حذف التطبيق",
|
||||
"sidebar.discovery": "اكتشاف",
|
||||
"sidebar.workspace": "مساحة العمل",
|
||||
"title": "استكشاف"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user