Compare commits
11 Commits
feat/searc
...
v0.7.8-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18dc3f8686 | ||
|
|
fe512005fc | ||
|
|
da131b6c59 | ||
|
|
dd23559d1f | ||
|
|
a6f0a8244f | ||
|
|
f04f8f53be | ||
|
|
a89a3f4146 | ||
|
|
55f5f2d11a | ||
|
|
0e8041bcac | ||
|
|
fc30482f65 | ||
|
|
6826c0ed43 |
@@ -3,6 +3,7 @@ name: Generate Unreleased Changelog PR
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * 1" # Runs every Monday at 00:00 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
generate-unreleased-changelog-pr:
|
||||
|
||||
35
.github/workflows/i18n-unused-keys.yml
vendored
35
.github/workflows/i18n-unused-keys.yml
vendored
@@ -39,12 +39,35 @@ jobs:
|
||||
# Check if each key is used in the source code
|
||||
for KEY in $KEYS; do
|
||||
FOUND=false
|
||||
for DIR in "${SOURCE_DIRS[@]}"; do
|
||||
if grep -r --include=\*.{js,jsx,ts,tsx} -q "$KEY" "$DIR"; then
|
||||
FOUND=true
|
||||
break
|
||||
|
||||
# Special case for dynamically constructed special variable keys
|
||||
if [[ "$KEY" == com_ui_special_var_* ]]; then
|
||||
# Check if TSpecialVarLabel is used in the codebase
|
||||
for DIR in "${SOURCE_DIRS[@]}"; do
|
||||
if grep -r --include=\*.{js,jsx,ts,tsx} -q "TSpecialVarLabel" "$DIR"; then
|
||||
FOUND=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Also check if the key is directly used somewhere
|
||||
if [[ "$FOUND" == false ]]; then
|
||||
for DIR in "${SOURCE_DIRS[@]}"; do
|
||||
if grep -r --include=\*.{js,jsx,ts,tsx} -q "$KEY" "$DIR"; then
|
||||
FOUND=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
else
|
||||
# Regular check for other keys
|
||||
for DIR in "${SOURCE_DIRS[@]}"; do
|
||||
if grep -r --include=\*.{js,jsx,ts,tsx} -q "$KEY" "$DIR"; then
|
||||
FOUND=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ "$FOUND" == false ]]; then
|
||||
UNUSED_KEYS+=("$KEY")
|
||||
@@ -90,4 +113,4 @@ jobs:
|
||||
|
||||
- name: Fail workflow if unused keys found
|
||||
if: env.unused_keys != '[]'
|
||||
run: exit 1
|
||||
run: exit 1
|
||||
|
||||
132
CHANGELOG.md
132
CHANGELOG.md
@@ -2,15 +2,141 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- 🪄 feat: Agent Artifacts by **@danny-avila** in [#5804](https://github.com/danny-avila/LibreChat/pull/5804)
|
||||
- 🔍 feat: Mistral OCR API / Upload Files as Text by **@danny-avila** in [#6274](https://github.com/danny-avila/LibreChat/pull/6274)
|
||||
- 🤖 feat: Support OpenAI Web Search models by **@danny-avila** in [#6313](https://github.com/danny-avila/LibreChat/pull/6313)
|
||||
- 🔗 feat: Agent Chain (Mixture-of-Agents) by **@danny-avila** in [#6374](https://github.com/danny-avila/LibreChat/pull/6374)
|
||||
- ⌛ feat: `initTimeout` for Slow Starting MCP Servers by **@perweij** in [#6383](https://github.com/danny-avila/LibreChat/pull/6383)
|
||||
- 🚀 feat: `S3` Integration for File handling and Image uploads by **@rubentalstra** in [#6142](https://github.com/danny-avila/LibreChat/pull/6142)
|
||||
- 🔒feat: Enable OpenID Auto-Redirect by **@leondape** in [#6066](https://github.com/danny-avila/LibreChat/pull/6066)
|
||||
- 🚀 feat: Integrate `Azure Blob Storage` for file handling and image uploads by **@rubentalstra** in [#6153](https://github.com/danny-avila/LibreChat/pull/6153)
|
||||
- 🚀 feat: Add support for custom `AWS` endpoint in `S3` by **@rubentalstra** in [#6431](https://github.com/danny-avila/LibreChat/pull/6431)
|
||||
- 🚀 feat: Add support for LDAP STARTTLS in LDAP authentication by **@rubentalstra** in [#6438](https://github.com/danny-avila/LibreChat/pull/6438)
|
||||
- 🚀 feat: Refactor schema exports and update package version to 0.0.4 by **@rubentalstra** in [#6455](https://github.com/danny-avila/LibreChat/pull/6455)
|
||||
- 🔼 feat: Add Auto Submit For URL Query Params by **@mjaverto** in [#6440](https://github.com/danny-avila/LibreChat/pull/6440)
|
||||
- 🛠 feat: Enhance Redis Integration, Rate Limiters & Log Headers by **@danny-avila** in [#6462](https://github.com/danny-avila/LibreChat/pull/6462)
|
||||
- 💵 feat: Add Automatic Balance Refill by **@rubentalstra** in [#6452](https://github.com/danny-avila/LibreChat/pull/6452)
|
||||
- 🗣️ feat: add support for gpt-4o-transcribe models by **@berry-13** in [#6483](https://github.com/danny-avila/LibreChat/pull/6483)
|
||||
- 🎨 feat: UI Refresh for Enhanced UX by **@berry-13** in [#6346](https://github.com/danny-avila/LibreChat/pull/6346)
|
||||
- 🌍 feat: Add support for Hungarian language localization by **@rubentalstra** in [#6508](https://github.com/danny-avila/LibreChat/pull/6508)
|
||||
- 🚀 feat: Add Gemini 2.5 Token/Context Values, Increase Max Possible Output to 64k by **@danny-avila** in [#6563](https://github.com/danny-avila/LibreChat/pull/6563)
|
||||
- 🚀 feat: Enhance MCP Connections For Multi-User Support by **@danny-avila** in [#6610](https://github.com/danny-avila/LibreChat/pull/6610)
|
||||
- 🚀 feat: Enhance S3 URL Expiry with Refresh; fix: S3 File Deletion by **@danny-avila** in [#6647](https://github.com/danny-avila/LibreChat/pull/6647)
|
||||
- 🚀 feat: enhance UI components and refactor settings by **@berry-13** in [#6625](https://github.com/danny-avila/LibreChat/pull/6625)
|
||||
- 💬 feat: move TemporaryChat to the Header by **@berry-13** in [#6646](https://github.com/danny-avila/LibreChat/pull/6646)
|
||||
- 🚀 feat: Use Model Specs + Specific Endpoints, Limit Providers for Agents by **@danny-avila** in [#6650](https://github.com/danny-avila/LibreChat/pull/6650)
|
||||
- 🪙 feat: Sync Balance Config on Login by **@danny-avila** in [#6671](https://github.com/danny-avila/LibreChat/pull/6671)
|
||||
- 🔦 feat: MCP Support for Non-Agent Endpoints by **@danny-avila** in [#6775](https://github.com/danny-avila/LibreChat/pull/6775)
|
||||
- 🗃️ feat: Code Interpreter File Persistence between Sessions by **@danny-avila** in [#6790](https://github.com/danny-avila/LibreChat/pull/6790)
|
||||
- 🖥️ feat: Code Interpreter API for Non-Agent Endpoints by **@danny-avila** in [#6803](https://github.com/danny-avila/LibreChat/pull/6803)
|
||||
- ⚡ feat: Self-hosted Artifacts Static Bundler URL by **@danny-avila** in [#6827](https://github.com/danny-avila/LibreChat/pull/6827)
|
||||
- 🐳 feat: Add Jemalloc and UV to Docker Builds by **@danny-avila** in [#6836](https://github.com/danny-avila/LibreChat/pull/6836)
|
||||
- 🤖 feat: GPT-4.1 by **@danny-avila** in [#6880](https://github.com/danny-avila/LibreChat/pull/6880)
|
||||
- 👋 feat: remove Edge TTS by **@berry-13** in [#6885](https://github.com/danny-avila/LibreChat/pull/6885)
|
||||
- feat: nav optimization by **@berry-13** in [#5785](https://github.com/danny-avila/LibreChat/pull/5785)
|
||||
- 🗺️ feat: Add Parameter Location Mapping for OpenAPI actions by **@peeeteeer** in [#6858](https://github.com/danny-avila/LibreChat/pull/6858)
|
||||
- 🤖 feat: Support `o4-mini` and `o3` Models by **@danny-avila** in [#6928](https://github.com/danny-avila/LibreChat/pull/6928)
|
||||
- 🎨 feat: OpenAI Image Tools (GPT-Image-1) by **@danny-avila** in [#7079](https://github.com/danny-avila/LibreChat/pull/7079)
|
||||
- 🗓️ feat: Add Special Variables for Prompts & Agents, Prompt UI Improvements by **@danny-avila** in [#7123](https://github.com/danny-avila/LibreChat/pull/7123)
|
||||
|
||||
### 🌍 Internationalization
|
||||
|
||||
- 🌍 i18n: Add Thai Language Support and Update Translations by **@rubentalstra** in [#6219](https://github.com/danny-avila/LibreChat/pull/6219)
|
||||
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6220](https://github.com/danny-avila/LibreChat/pull/6220)
|
||||
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6240](https://github.com/danny-avila/LibreChat/pull/6240)
|
||||
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6241](https://github.com/danny-avila/LibreChat/pull/6241)
|
||||
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6277](https://github.com/danny-avila/LibreChat/pull/6277)
|
||||
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6414](https://github.com/danny-avila/LibreChat/pull/6414)
|
||||
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6505](https://github.com/danny-avila/LibreChat/pull/6505)
|
||||
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6530](https://github.com/danny-avila/LibreChat/pull/6530)
|
||||
- 🌍 i18n: Add Persian Localization Support by **@rubentalstra** in [#6669](https://github.com/danny-avila/LibreChat/pull/6669)
|
||||
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#6667](https://github.com/danny-avila/LibreChat/pull/6667)
|
||||
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7126](https://github.com/danny-avila/LibreChat/pull/7126)
|
||||
- 🌍 i18n: Update translation.json with latest translations by **@github-actions[bot]** in [#7148](https://github.com/danny-avila/LibreChat/pull/7148)
|
||||
|
||||
### 👐 Accessibility
|
||||
|
||||
- 🎨 a11y: Update Model Spec Description Text by **@berry-13** in [#6294](https://github.com/danny-avila/LibreChat/pull/6294)
|
||||
- 🗑️ a11y: Add Accessible Name to Button for File Attachment Removal by **@kangabell** in [#6709](https://github.com/danny-avila/LibreChat/pull/6709)
|
||||
- ⌨️ a11y: enhance accessibility & visual consistency by **@berry-13** in [#6866](https://github.com/danny-avila/LibreChat/pull/6866)
|
||||
- 🙌 a11y: Searchbar/Conversations List Focus by **@danny-avila** in [#7096](https://github.com/danny-avila/LibreChat/pull/7096)
|
||||
- 👐 a11y: Improve Fork and SplitText Accessibility by **@danny-avila** in [#7147](https://github.com/danny-avila/LibreChat/pull/7147)
|
||||
|
||||
### 🔧 Fixes
|
||||
|
||||
- 🐛 fix: Avatar Type Definitions in Agent/Assistant Schemas by **@danny-avila** in [#6235](https://github.com/danny-avila/LibreChat/pull/6235)
|
||||
- 🔧 fix: MeiliSearch Field Error and Patch Incorrect Import by #6210 by **@rubentalstra** in [#6245](https://github.com/danny-avila/LibreChat/pull/6245)
|
||||
- 🔏 fix: Enhance Two-Factor Authentication by **@rubentalstra** in [#6247](https://github.com/danny-avila/LibreChat/pull/6247)
|
||||
- 🐛 fix: Await saveMessage in abortMiddleware to ensure proper execution by **@sh4shii** in [#6248](https://github.com/danny-avila/LibreChat/pull/6248)
|
||||
- 🔧 fix: Axios Proxy Usage And Bump `mongoose` by **@danny-avila** in [#6298](https://github.com/danny-avila/LibreChat/pull/6298)
|
||||
- 🔧 fix: comment out MCP servers to resolve service run issues by **@KunalScriptz** in [#6316](https://github.com/danny-avila/LibreChat/pull/6316)
|
||||
- 🔧 fix: Update Token Calculations and Mapping, MCP `env` Initialization by **@danny-avila** in [#6406](https://github.com/danny-avila/LibreChat/pull/6406)
|
||||
- 🐞 fix: Agent "Resend" Message Attachments + Source Icon Styling by **@danny-avila** in [#6408](https://github.com/danny-avila/LibreChat/pull/6408)
|
||||
- 🐛 fix: Prevent Crash on Duplicate Message ID by **@Odrec** in [#6392](https://github.com/danny-avila/LibreChat/pull/6392)
|
||||
- 🔐 fix: Invalid Key Length in 2FA Encryption by **@rubentalstra** in [#6432](https://github.com/danny-avila/LibreChat/pull/6432)
|
||||
- 🏗️ fix: Fix Agents Token Spend Race Conditions, Expand Test Coverage by **@danny-avila** in [#6480](https://github.com/danny-avila/LibreChat/pull/6480)
|
||||
- 🔃 fix: Draft Clearing, Claude Titles, Remove Default Vision Max Tokens by **@danny-avila** in [#6501](https://github.com/danny-avila/LibreChat/pull/6501)
|
||||
- 🔧 fix: Update username reference to use user.name in greeting display by **@rubentalstra** in [#6534](https://github.com/danny-avila/LibreChat/pull/6534)
|
||||
- 🔧 fix: S3 Download Stream with Key Extraction and Blob Storage Encoding for Vision by **@danny-avila** in [#6557](https://github.com/danny-avila/LibreChat/pull/6557)
|
||||
- 🔧 fix: Mistral type strictness for `usage` & update token values/windows by **@danny-avila** in [#6562](https://github.com/danny-avila/LibreChat/pull/6562)
|
||||
- 🔧 fix: Consolidate Text Parsing and TTS Edge Initialization by **@danny-avila** in [#6582](https://github.com/danny-avila/LibreChat/pull/6582)
|
||||
- 🔧 fix: Ensure continuation in image processing on base64 encoding from Blob Storage by **@danny-avila** in [#6619](https://github.com/danny-avila/LibreChat/pull/6619)
|
||||
- ✉️ fix: Fallback For User Name In Email Templates by **@danny-avila** in [#6620](https://github.com/danny-avila/LibreChat/pull/6620)
|
||||
- 🔧 fix: Azure Blob Integration and File Source References by **@rubentalstra** in [#6575](https://github.com/danny-avila/LibreChat/pull/6575)
|
||||
- 🐛 fix: Safeguard against undefined addedEndpoints by **@wipash** in [#6654](https://github.com/danny-avila/LibreChat/pull/6654)
|
||||
- 🤖 fix: Gemini 2.5 Vision Support by **@danny-avila** in [#6663](https://github.com/danny-avila/LibreChat/pull/6663)
|
||||
- 🔄 fix: Avatar & Error Handling Enhancements by **@danny-avila** in [#6687](https://github.com/danny-avila/LibreChat/pull/6687)
|
||||
- 🔧 fix: Chat Middleware, Zod Conversion, Auto-Save and S3 URL Refresh by **@danny-avila** in [#6720](https://github.com/danny-avila/LibreChat/pull/6720)
|
||||
- 🔧 fix: Agent Capability Checks & DocumentDB Compatibility for Agent Resource Removal by **@danny-avila** in [#6726](https://github.com/danny-avila/LibreChat/pull/6726)
|
||||
- 🔄 fix: Improve audio MIME type detection and handling by **@berry-13** in [#6707](https://github.com/danny-avila/LibreChat/pull/6707)
|
||||
- 🪺 fix: Update Role Handling due to New Schema Shape by **@danny-avila** in [#6774](https://github.com/danny-avila/LibreChat/pull/6774)
|
||||
- 🗨️ fix: Show ModelSpec Greeting by **@berry-13** in [#6770](https://github.com/danny-avila/LibreChat/pull/6770)
|
||||
- 🔧 fix: Keyv and Proxy Issues, and More Memory Optimizations by **@danny-avila** in [#6867](https://github.com/danny-avila/LibreChat/pull/6867)
|
||||
- ✨ fix: Implement dynamic text sizing for greeting and name display by **@berry-13** in [#6833](https://github.com/danny-avila/LibreChat/pull/6833)
|
||||
- 📝 fix: Mistral OCR Image Support and Azure Agent Titles by **@danny-avila** in [#6901](https://github.com/danny-avila/LibreChat/pull/6901)
|
||||
- 📢 fix: Invalid `engineTTS` and Conversation State on Navigation by **@berry-13** in [#6904](https://github.com/danny-avila/LibreChat/pull/6904)
|
||||
- 🛠️ fix: Improve Accessibility and Display of Conversation Menu by **@danny-avila** in [#6913](https://github.com/danny-avila/LibreChat/pull/6913)
|
||||
- 🔧 fix: Agent Resource Form, Convo Menu Style, Ensure Draft Clears on Submission by **@danny-avila** in [#6925](https://github.com/danny-avila/LibreChat/pull/6925)
|
||||
- 🔀 fix: MCP Improvements, Auto-Save Drafts, Artifact Markup by **@danny-avila** in [#7040](https://github.com/danny-avila/LibreChat/pull/7040)
|
||||
- 🐋 fix: Improve Deepseek Compatbility by **@danny-avila** in [#7132](https://github.com/danny-avila/LibreChat/pull/7132)
|
||||
- 🐙 fix: Add Redis Ping Interval to Prevent Connection Drops by **@peeeteeer** in [#7127](https://github.com/danny-avila/LibreChat/pull/7127)
|
||||
|
||||
### ⚙️ Other Changes
|
||||
|
||||
- 🔄 chore: Enforce 18next Language Keys by **@rubentalstra** in [#5803](https://github.com/danny-avila/LibreChat/pull/5803)
|
||||
- 🔃 refactor: Parent Message ID Handling on Error, Update Translations, Bump Agents by **@danny-avila** in [#5833](https://github.com/danny-avila/LibreChat/pull/5833)
|
||||
- 📦 refactor: Move DB Models to `@librechat/data-schemas` by **@rubentalstra** in [#6210](https://github.com/danny-avila/LibreChat/pull/6210)
|
||||
- 📦 chore: Patch `axios` to address CVE-2025-27152 by **@danny-avila** in [#6222](https://github.com/danny-avila/LibreChat/pull/6222)
|
||||
- ⚠️ refactor: Use Error Content Part Instead Of Throwing Error for Agents by **@danny-avila** in [#6262](https://github.com/danny-avila/LibreChat/pull/6262)
|
||||
- 🏃♂️ refactor: Improve Agent Run Context & Misc. Changes by **@danny-avila** in [#6448](https://github.com/danny-avila/LibreChat/pull/6448)
|
||||
- 📝 docs: librechat.example.yaml by **@ineiti** in [#6442](https://github.com/danny-avila/LibreChat/pull/6442)
|
||||
- 🏃♂️ refactor: More Agent Context Improvements during Run by **@danny-avila** in [#6477](https://github.com/danny-avila/LibreChat/pull/6477)
|
||||
- 🔃 refactor: Allow streaming for `o1` models by **@danny-avila** in [#6509](https://github.com/danny-avila/LibreChat/pull/6509)
|
||||
- 🔧 chore: `Vite` Plugin Upgrades & Config Optimizations by **@rubentalstra** in [#6547](https://github.com/danny-avila/LibreChat/pull/6547)
|
||||
- 🔧 refactor: Consolidate Logging, Model Selection & Actions Optimizations, Minor Fixes by **@danny-avila** in [#6553](https://github.com/danny-avila/LibreChat/pull/6553)
|
||||
- 🎨 style: Address Minor UI Refresh Issues by **@berry-13** in [#6552](https://github.com/danny-avila/LibreChat/pull/6552)
|
||||
- 🔧 refactor: Enhance Model & Endpoint Configurations with Global Indicators 🌍 by **@berry-13** in [#6578](https://github.com/danny-avila/LibreChat/pull/6578)
|
||||
- 💬 style: Chat UI, Greeting, and Message adjustments by **@berry-13** in [#6612](https://github.com/danny-avila/LibreChat/pull/6612)
|
||||
- ⚡ refactor: DocumentDB Compatibility for Balance Updates by **@danny-avila** in [#6673](https://github.com/danny-avila/LibreChat/pull/6673)
|
||||
- 🧹 chore: Update ESLint rules for React hooks by **@rubentalstra** in [#6685](https://github.com/danny-avila/LibreChat/pull/6685)
|
||||
- 🪙 chore: Update Gemini Pricing by **@RedwindA** in [#6731](https://github.com/danny-avila/LibreChat/pull/6731)
|
||||
- 🪺 refactor: Nest Permission fields for Roles by **@rubentalstra** in [#6487](https://github.com/danny-avila/LibreChat/pull/6487)
|
||||
- 📦 chore: Update `caniuse-lite` dependency to version 1.0.30001706 by **@rubentalstra** in [#6482](https://github.com/danny-avila/LibreChat/pull/6482)
|
||||
- ⚙️ refactor: OAuth Flow Signal, Type Safety, Tool Progress & Updated Packages by **@danny-avila** in [#6752](https://github.com/danny-avila/LibreChat/pull/6752)
|
||||
- 📦 chore: bump vite from 6.2.3 to 6.2.5 by **@dependabot[bot]** in [#6745](https://github.com/danny-avila/LibreChat/pull/6745)
|
||||
- 💾 chore: Enhance Local Storage Handling and Update MCP SDK by **@danny-avila** in [#6809](https://github.com/danny-avila/LibreChat/pull/6809)
|
||||
- 🤖 refactor: Improve Agents Memory Usage, Bump Keyv, Grok 3 by **@danny-avila** in [#6850](https://github.com/danny-avila/LibreChat/pull/6850)
|
||||
- 💾 refactor: Enhance Memory In Image Encodings & Client Disposal by **@danny-avila** in [#6852](https://github.com/danny-avila/LibreChat/pull/6852)
|
||||
- 🔁 refactor: Token Event Handler and Standardize `maxTokens` Key by **@danny-avila** in [#6886](https://github.com/danny-avila/LibreChat/pull/6886)
|
||||
- 🔍 refactor: Search & Message Retrieval by **@berry-13** in [#6903](https://github.com/danny-avila/LibreChat/pull/6903)
|
||||
- 🎨 style: standardize dropdown styling & fix z-Index layering by **@berry-13** in [#6939](https://github.com/danny-avila/LibreChat/pull/6939)
|
||||
- 📙 docs: CONTRIBUTING.md by **@dblock** in [#6831](https://github.com/danny-avila/LibreChat/pull/6831)
|
||||
- 🧭 refactor: Modernize Nav/Header by **@danny-avila** in [#7094](https://github.com/danny-avila/LibreChat/pull/7094)
|
||||
- 🪶 refactor: Chat Input Focus for Conversation Navigations & ChatForm Optimizations by **@danny-avila** in [#7100](https://github.com/danny-avila/LibreChat/pull/7100)
|
||||
- 🔃 refactor: Streamline Navigation, Message Loading UX by **@danny-avila** in [#7118](https://github.com/danny-avila/LibreChat/pull/7118)
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# v0.7.7
|
||||
# v0.7.8-rc1
|
||||
|
||||
# Base node image
|
||||
FROM node:20-alpine AS node
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Dockerfile.multi
|
||||
# v0.7.7
|
||||
# v0.7.8-rc1
|
||||
|
||||
# Base for all builds
|
||||
FROM node:20-alpine AS base-min
|
||||
|
||||
7
api/cache/keyvRedis.js
vendored
7
api/cache/keyvRedis.js
vendored
@@ -75,6 +75,12 @@ if (REDIS_URI && isEnabled(USE_REDIS)) {
|
||||
} else {
|
||||
keyvRedis = new KeyvRedis(REDIS_URI, keyvOpts);
|
||||
}
|
||||
|
||||
const pingInterval = setInterval(() => {
|
||||
logger.debug('KeyvRedis ping');
|
||||
keyvRedis.client.ping().catch(err => logger.error('Redis keep-alive ping failed:', err));
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
keyvRedis.on('ready', () => {
|
||||
logger.info('KeyvRedis connection ready');
|
||||
});
|
||||
@@ -85,6 +91,7 @@ if (REDIS_URI && isEnabled(USE_REDIS)) {
|
||||
logger.info('KeyvRedis connection ended');
|
||||
});
|
||||
keyvRedis.on('close', () => {
|
||||
clearInterval(pingInterval);
|
||||
logger.info('KeyvRedis connection closed');
|
||||
});
|
||||
keyvRedis.on('error', (err) => logger.error('KeyvRedis connection error:', err));
|
||||
|
||||
@@ -308,7 +308,7 @@ const getListAgents = async (searchParameter) => {
|
||||
* This function also updates the corresponding projects to include or exclude the agent ID.
|
||||
*
|
||||
* @param {Object} params - Parameters for updating the agent's projects.
|
||||
* @param {import('librechat-data-provider').TUser} params.user - Parameters for updating the agent's projects.
|
||||
* @param {MongoUser} params.user - Parameters for updating the agent's projects.
|
||||
* @param {string} params.agentId - The ID of the agent to update.
|
||||
* @param {string[]} [params.projectIds] - Array of project IDs to add to the agent.
|
||||
* @param {string[]} [params.removeProjectIds] - Array of project IDs to remove from the agent.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@librechat/backend",
|
||||
"version": "v0.7.7",
|
||||
"version": "v0.7.8-rc1",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start": "echo 'please run this from the root directory'",
|
||||
|
||||
@@ -128,7 +128,7 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
||||
clientRef = new WeakRef(client);
|
||||
|
||||
getAbortData = () => {
|
||||
const currentClient = clientRef.deref();
|
||||
const currentClient = clientRef?.deref();
|
||||
const currentText =
|
||||
currentClient?.getStreamText != null ? currentClient.getStreamText() : getPartialText();
|
||||
|
||||
@@ -255,7 +255,7 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
||||
logger.error('[AskController] Error handling request', error);
|
||||
let partialText = '';
|
||||
try {
|
||||
const currentClient = clientRef.deref();
|
||||
const currentClient = clientRef?.deref();
|
||||
partialText =
|
||||
currentClient?.getStreamText != null ? currentClient.getStreamText() : getPartialText();
|
||||
} catch (getTextError) {
|
||||
@@ -268,6 +268,7 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
||||
conversationId: reqDataContext.conversationId,
|
||||
messageId: reqDataContext.responseMessageId,
|
||||
parentMessageId: overrideParentMessageId ?? reqDataContext.userMessageId ?? parentMessageId,
|
||||
userMessageId: reqDataContext.userMessageId,
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error('[AskController] Error in `handleAbortError` during catch block', err);
|
||||
|
||||
@@ -123,7 +123,7 @@ const EditController = async (req, res, next, initializeClient) => {
|
||||
clientRef = new WeakRef(client);
|
||||
|
||||
getAbortData = () => {
|
||||
const currentClient = clientRef.deref();
|
||||
const currentClient = clientRef?.deref();
|
||||
const currentText =
|
||||
currentClient?.getStreamText != null ? currentClient.getStreamText() : getPartialText();
|
||||
|
||||
@@ -219,7 +219,7 @@ const EditController = async (req, res, next, initializeClient) => {
|
||||
logger.error('[EditController] Error handling request', error);
|
||||
let partialText = '';
|
||||
try {
|
||||
const currentClient = clientRef.deref();
|
||||
const currentClient = clientRef?.deref();
|
||||
partialText =
|
||||
currentClient?.getStreamText != null ? currentClient.getStreamText() : getPartialText();
|
||||
} catch (getTextError) {
|
||||
@@ -232,6 +232,7 @@ const EditController = async (req, res, next, initializeClient) => {
|
||||
conversationId,
|
||||
messageId: reqDataContext.responseMessageId,
|
||||
parentMessageId: overrideParentMessageId ?? userMessageId ?? parentMessageId,
|
||||
userMessageId,
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error('[EditController] Error in `handleAbortError` during catch block', err);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { CacheKeys, AuthType } = require('librechat-data-provider');
|
||||
const { addOpenAPISpecs } = require('~/app/clients/tools/util/addOpenAPISpecs');
|
||||
const { getToolkitKey } = require('~/server/services/ToolService');
|
||||
const { getCustomConfig } = require('~/server/services/Config');
|
||||
const { availableTools } = require('~/app/clients/tools');
|
||||
const { getMCPManager } = require('~/config');
|
||||
@@ -128,7 +129,7 @@ const getAvailableTools = async (req, res) => {
|
||||
(plugin) =>
|
||||
toolDefinitions[plugin.pluginKey] !== undefined ||
|
||||
(plugin.toolkit === true &&
|
||||
Object.keys(toolDefinitions).some((key) => key.startsWith(`${plugin.pluginKey}_`))),
|
||||
Object.keys(toolDefinitions).some((key) => getToolkitKey(key) === plugin.pluginKey)),
|
||||
);
|
||||
|
||||
await cache.set(CacheKeys.TOOLS, tools);
|
||||
|
||||
@@ -673,7 +673,7 @@ class AgentClient extends BaseClient {
|
||||
this.indexTokenCountMap,
|
||||
toolSet,
|
||||
);
|
||||
if (legacyContentEndpoints.has(this.options.agent.endpoint)) {
|
||||
if (legacyContentEndpoints.has(this.options.agent.endpoint?.toLowerCase())) {
|
||||
initialMessages = formatContentStrings(initialMessages);
|
||||
}
|
||||
|
||||
|
||||
@@ -259,6 +259,7 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
||||
sender,
|
||||
messageId: responseMessageId,
|
||||
parentMessageId: overrideParentMessageId ?? userMessageId ?? parentMessageId,
|
||||
userMessageId,
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error('[api/server/controllers/agents/request] Error in `handleAbortError`', err);
|
||||
|
||||
@@ -311,7 +311,7 @@ const handleAbortError = async (res, req, error, data) => {
|
||||
} else {
|
||||
logger.error('[handleAbortError] AI response error; aborting request:', error);
|
||||
}
|
||||
const { sender, conversationId, messageId, parentMessageId, partialText } = data;
|
||||
const { sender, conversationId, messageId, parentMessageId, userMessageId, partialText } = data;
|
||||
|
||||
if (error.stack && error.stack.includes('google')) {
|
||||
logger.warn(
|
||||
@@ -344,10 +344,10 @@ const handleAbortError = async (res, req, error, data) => {
|
||||
parentMessageId,
|
||||
text: errorText,
|
||||
user: req.user.id,
|
||||
shouldSaveMessage: true,
|
||||
spec: endpointOption?.spec,
|
||||
iconURL: endpointOption?.iconURL,
|
||||
modelLabel: endpointOption?.modelLabel,
|
||||
shouldSaveMessage: userMessageId != null,
|
||||
model: endpointOption?.modelOptions?.model || req.body?.model,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,40 +1,17 @@
|
||||
const { Keyv } = require('keyv');
|
||||
const express = require('express');
|
||||
const { MeiliSearch } = require('meilisearch');
|
||||
const { Conversation } = require('~/models/Conversation');
|
||||
const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
|
||||
const { Message } = require('~/models/Message');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const keyvRedis = require('~/cache/keyvRedis');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const expiration = 60 * 1000;
|
||||
const cache = isEnabled(process.env.USE_REDIS)
|
||||
? new Keyv({ store: keyvRedis })
|
||||
: new Keyv({ namespace: 'search', ttl: expiration });
|
||||
|
||||
router.use(requireJwtAuth);
|
||||
|
||||
router.get('/sync', async function (req, res) {
|
||||
await Message.syncWithMeili();
|
||||
await Conversation.syncWithMeili();
|
||||
res.send('synced');
|
||||
});
|
||||
|
||||
router.get('/test', async function (req, res) {
|
||||
const { q } = req.query;
|
||||
const messages = (
|
||||
await Message.meiliSearch(q, { attributesToHighlight: ['text'] }, true)
|
||||
).hits.map((message) => {
|
||||
const { _formatted, ...rest } = message;
|
||||
return { ...rest, searchResult: true, text: _formatted.text };
|
||||
});
|
||||
res.send(messages);
|
||||
});
|
||||
|
||||
router.get('/enable', async function (req, res) {
|
||||
let result = false;
|
||||
if (!isEnabled(process.env.SEARCH)) {
|
||||
return res.send(false);
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new MeiliSearch({
|
||||
host: process.env.MEILI_HOST,
|
||||
@@ -42,8 +19,7 @@ router.get('/enable', async function (req, res) {
|
||||
});
|
||||
|
||||
const { status } = await client.health();
|
||||
result = status === 'available' && !!process.env.SEARCH;
|
||||
return res.send(result);
|
||||
return res.send(status === 'available');
|
||||
} catch (error) {
|
||||
return res.send(false);
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ async function createActionTool({
|
||||
/** @type {import('librechat-data-provider').ActionMetadataRuntime} */
|
||||
const metadata = action.metadata;
|
||||
const executor = requestBuilder.createExecutor();
|
||||
const preparedExecutor = executor.setParams(toolInput);
|
||||
const preparedExecutor = executor.setParams(toolInput ?? {});
|
||||
|
||||
if (metadata.auth && metadata.auth.type !== AuthTypeEnum.None) {
|
||||
try {
|
||||
|
||||
@@ -6,6 +6,7 @@ const {
|
||||
EToolResources,
|
||||
getResponseSender,
|
||||
AgentCapabilities,
|
||||
replaceSpecialVars,
|
||||
providerEndpointMap,
|
||||
} = require('librechat-data-provider');
|
||||
const {
|
||||
@@ -246,6 +247,13 @@ const initializeAgentOptions = async ({
|
||||
agent.model_parameters.model = agent.model;
|
||||
}
|
||||
|
||||
if (agent.instructions && agent.instructions !== '') {
|
||||
agent.instructions = replaceSpecialVars({
|
||||
text: agent.instructions,
|
||||
user: req.user,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof agent.artifacts === 'string' && agent.artifacts !== '') {
|
||||
agent.additional_instructions = generateArtifactsPrompt({
|
||||
endpoint: agent.provider,
|
||||
|
||||
@@ -8,6 +8,7 @@ const {
|
||||
ErrorTypes,
|
||||
ContentTypes,
|
||||
imageGenTools,
|
||||
EToolResources,
|
||||
EModelEndpoint,
|
||||
actionDelimiter,
|
||||
ImageVisionTool,
|
||||
@@ -36,6 +37,30 @@ const { redactMessage } = require('~/config/parsers');
|
||||
const { sleep } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
* @param {string} toolName
|
||||
* @returns {string | undefined} toolKey
|
||||
*/
|
||||
function getToolkitKey(toolName) {
|
||||
/** @type {string|undefined} */
|
||||
let toolkitKey;
|
||||
for (const toolkit of toolkits) {
|
||||
if (toolName.startsWith(EToolResources.image_edit)) {
|
||||
const splitMatches = toolkit.pluginKey.split('_');
|
||||
const suffix = splitMatches[splitMatches.length - 1];
|
||||
if (toolName.endsWith(suffix)) {
|
||||
toolkitKey = toolkit.pluginKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (toolName.startsWith(toolkit.pluginKey)) {
|
||||
toolkitKey = toolkit.pluginKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return toolkitKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and formats tools from the specified tool directory.
|
||||
*
|
||||
@@ -108,7 +133,7 @@ function loadAndFormatTools({ directory, adminFilter = [], adminIncluded = [] })
|
||||
tools.push(formattedTool);
|
||||
}
|
||||
|
||||
/** Basic Tools; schema: { input: string } */
|
||||
/** Basic Tools & Toolkits; schema: { input: string } */
|
||||
const basicToolInstances = [
|
||||
new Calculator(),
|
||||
...createOpenAIImageTools({ override: true }),
|
||||
@@ -117,9 +142,7 @@ function loadAndFormatTools({ directory, adminFilter = [], adminIncluded = [] })
|
||||
for (const toolInstance of basicToolInstances) {
|
||||
const formattedTool = formatToOpenAIAssistantTool(toolInstance);
|
||||
let toolName = formattedTool[Tools.function].name;
|
||||
toolName = toolkits.some((toolkit) => toolName.startsWith(toolkit.pluginKey))
|
||||
? toolName.split('_')[0]
|
||||
: toolName;
|
||||
toolName = getToolkitKey(toolName) ?? toolName;
|
||||
if (filter.has(toolName) && included.size === 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -682,6 +705,7 @@ async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey })
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getToolkitKey,
|
||||
loadAgentTools,
|
||||
loadAndFormatTools,
|
||||
processRequiredActions,
|
||||
|
||||
@@ -70,7 +70,13 @@ const sendError = async (req, res, options, callback) => {
|
||||
}
|
||||
|
||||
if (shouldSaveMessage) {
|
||||
await saveMessage(req, { ...errorMessage, user });
|
||||
await saveMessage(
|
||||
req,
|
||||
{ ...errorMessage, user },
|
||||
{
|
||||
context: 'api/server/utils/streamResponse.js - sendError',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (!errorMessage.error) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@librechat/frontend",
|
||||
"version": "v0.7.7",
|
||||
"version": "v0.7.8-rc1",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -2,22 +2,33 @@ import { memo, useCallback } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useGetMessagesByConvoId } from 'librechat-data-provider/react-query';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import type { ChatFormValues } from '~/common';
|
||||
import { ChatContext, AddedChatContext, useFileMapContext, ChatFormProvider } from '~/Providers';
|
||||
import { useChatHelpers, useAddedResponse, useSSE } from '~/hooks';
|
||||
import ConversationStarters from './Input/ConversationStarters';
|
||||
import { useGetMessagesByConvoId } from '~/data-provider';
|
||||
import MessagesView from './Messages/MessagesView';
|
||||
import { Spinner } from '~/components/svg';
|
||||
import Presentation from './Presentation';
|
||||
import { buildTree, cn } from '~/utils';
|
||||
import ChatForm from './Input/ChatForm';
|
||||
import { buildTree } from '~/utils';
|
||||
import Landing from './Landing';
|
||||
import Header from './Header';
|
||||
import Footer from './Footer';
|
||||
import store from '~/store';
|
||||
|
||||
function LoadingSpinner() {
|
||||
return (
|
||||
<div className="relative flex-1 overflow-hidden overflow-y-auto">
|
||||
<div className="relative flex h-full items-center justify-center">
|
||||
<Spinner className="text-text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatView({ index = 0 }: { index?: number }) {
|
||||
const { conversationId } = useParams();
|
||||
const rootSubmission = useRecoilValue(store.submissionByIndex(index));
|
||||
@@ -48,16 +59,15 @@ function ChatView({ index = 0 }: { index?: number }) {
|
||||
});
|
||||
|
||||
let content: JSX.Element | null | undefined;
|
||||
const isLandingPage = !messagesTree || messagesTree.length === 0;
|
||||
const isLandingPage =
|
||||
(!messagesTree || messagesTree.length === 0) &&
|
||||
(conversationId === Constants.NEW_CONVO || !conversationId);
|
||||
const isNavigating = (!messagesTree || messagesTree.length === 0) && conversationId != null;
|
||||
|
||||
if (isLoading && conversationId !== 'new') {
|
||||
content = (
|
||||
<div className="relative flex-1 overflow-hidden overflow-y-auto">
|
||||
<div className="relative flex h-full items-center justify-center">
|
||||
<Spinner className="text-text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
if (isLoading && conversationId !== Constants.NEW_CONVO) {
|
||||
content = <LoadingSpinner />;
|
||||
} else if ((isLoading || isNavigating) && !isLandingPage) {
|
||||
content = <LoadingSpinner />;
|
||||
} else if (!isLandingPage) {
|
||||
content = <MessagesView messagesTree={messagesTree} />;
|
||||
} else {
|
||||
@@ -71,27 +81,28 @@ function ChatView({ index = 0 }: { index?: number }) {
|
||||
<Presentation>
|
||||
<div className="flex h-full w-full flex-col">
|
||||
{!isLoading && <Header />}
|
||||
|
||||
{isLandingPage ? (
|
||||
<>
|
||||
<div className="flex flex-1 flex-col items-center justify-end sm:justify-center">
|
||||
{content}
|
||||
<div className="w-full max-w-3xl transition-all duration-200 xl:max-w-4xl">
|
||||
<ChatForm index={index} />
|
||||
<ConversationStarters />
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full flex-col overflow-y-auto">
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col',
|
||||
isLandingPage
|
||||
? 'flex-1 items-center justify-end sm:justify-center'
|
||||
: 'h-full overflow-y-auto',
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
<div className="w-full">
|
||||
<div
|
||||
className={cn(
|
||||
'w-full',
|
||||
isLandingPage && 'max-w-3xl transition-all duration-200 xl:max-w-4xl',
|
||||
)}
|
||||
>
|
||||
<ChatForm index={index} />
|
||||
<Footer />
|
||||
{isLandingPage ? <ConversationStarters /> : <Footer />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isLandingPage && <Footer />}
|
||||
</>
|
||||
</div>
|
||||
</Presentation>
|
||||
</AddedChatContext.Provider>
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function Header() {
|
||||
return (
|
||||
<div className="sticky top-0 z-10 flex h-14 w-full items-center justify-between bg-white p-2 font-semibold text-text-primary dark:bg-gray-800">
|
||||
<div className="hide-scrollbar flex w-full items-center justify-between gap-2 overflow-x-auto">
|
||||
<div className="mx-2 flex items-center gap-2">
|
||||
<div className="mx-1 flex items-center gap-2">
|
||||
{!navVisible && <OpenSidebar setNavVisible={setNavVisible} />}
|
||||
{!navVisible && <HeaderNewChat />}
|
||||
{<ModelSelector startupConfig={startupConfig} />}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
useHandleKeyUp,
|
||||
useQueryParams,
|
||||
useSubmitMessage,
|
||||
useFocusChatEffect,
|
||||
} from '~/hooks';
|
||||
import { mainTextareaId, BadgeItem } from '~/common';
|
||||
import AttachFileChat from './Files/AttachFileChat';
|
||||
@@ -36,6 +37,7 @@ import store from '~/store';
|
||||
const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||
const submitButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
useFocusChatEffect(textAreaRef);
|
||||
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [, setIsScrollable] = useState(false);
|
||||
@@ -43,7 +45,6 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false);
|
||||
const [backupBadges, setBackupBadges] = useState<Pick<BadgeItem, 'id'>[]>([]);
|
||||
|
||||
const search = useRecoilValue(store.search);
|
||||
const SpeechToText = useRecoilValue(store.speechToText);
|
||||
const TextToSpeech = useRecoilValue(store.textToSpeech);
|
||||
const chatDirection = useRecoilValue(store.chatDirection);
|
||||
@@ -131,7 +132,13 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||
setShowPlusPopover,
|
||||
setShowMentionPopover,
|
||||
});
|
||||
const { handlePaste, handleKeyDown, handleCompositionStart, handleCompositionEnd } = useTextarea({
|
||||
const {
|
||||
isNotAppendable,
|
||||
handlePaste,
|
||||
handleKeyDown,
|
||||
handleCompositionStart,
|
||||
handleCompositionEnd,
|
||||
} = useTextarea({
|
||||
textAreaRef,
|
||||
submitButtonRef,
|
||||
setIsScrollable,
|
||||
@@ -151,12 +158,6 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||
|
||||
const textValue = useWatch({ control: methods.control, name: 'text' });
|
||||
|
||||
useEffect(() => {
|
||||
if (!search.isSearching && textAreaRef.current && !disableInputs) {
|
||||
textAreaRef.current.focus();
|
||||
}
|
||||
}, [search.isSearching, disableInputs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (textAreaRef.current) {
|
||||
const style = window.getComputedStyle(textAreaRef.current);
|
||||
@@ -256,7 +257,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||
ref(e);
|
||||
(textAreaRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = e;
|
||||
}}
|
||||
disabled={disableInputs}
|
||||
disabled={disableInputs || isNotAppendable}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
@@ -276,7 +277,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||
className={cn(
|
||||
baseClasses,
|
||||
removeFocusRings,
|
||||
'transition-[max-height] duration-200',
|
||||
'transition-[max-height] duration-200 disabled:cursor-not-allowed',
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col items-start justify-start pt-1.5">
|
||||
@@ -311,7 +312,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||
methods={methods}
|
||||
ask={submitMessage}
|
||||
textAreaRef={textAreaRef}
|
||||
disabled={disableInputs}
|
||||
disabled={disableInputs || isNotAppendable}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
)}
|
||||
@@ -323,7 +324,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||
<SendButton
|
||||
ref={submitButtonRef}
|
||||
control={methods.control}
|
||||
disabled={filesLoading || isSubmitting || disableInputs}
|
||||
disabled={filesLoading || isSubmitting || disableInputs || isNotAppendable}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -201,7 +201,7 @@ function PromptsCommand({
|
||||
<div className="popover border-token-border-light rounded-2xl border bg-surface-tertiary-alt p-2 shadow-lg">
|
||||
<input
|
||||
// The user expects focus to transition to the input field when the popover is opened
|
||||
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
placeholder={localize('com_ui_command_usage_placeholder')}
|
||||
|
||||
@@ -20,6 +20,7 @@ export default function HeaderNewChat() {
|
||||
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
|
||||
[],
|
||||
);
|
||||
queryClient.invalidateQueries([QueryKeys.messages]);
|
||||
newConversation();
|
||||
};
|
||||
|
||||
|
||||
@@ -14,10 +14,9 @@ export default function MessagesView({
|
||||
messagesTree?: TMessage[] | null;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const scrollButtonPreference = useRecoilValue(store.showScrollButton);
|
||||
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
||||
const fontSize = useRecoilValue(store.fontSize);
|
||||
const { screenshotTargetRef } = useScreenshot();
|
||||
const scrollButtonPreference = useRecoilValue(store.showScrollButton);
|
||||
const [currentEditId, setCurrentEditId] = useState<number | string | null>(-1);
|
||||
|
||||
const {
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function SearchButtons({ message }: { message: TMessage }) {
|
||||
const localize = useLocalize();
|
||||
const queryClient = useQueryClient();
|
||||
const search = useRecoilValue(store.search);
|
||||
const { navigateWithLastTools } = useNavigateToConvo();
|
||||
const { navigateToConvo } = useNavigateToConvo();
|
||||
const conversationId = message.conversationId ?? '';
|
||||
|
||||
const clickHandler = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
@@ -39,14 +39,13 @@ export default function SearchButtons({ message }: { message: TMessage }) {
|
||||
}
|
||||
|
||||
document.title = title;
|
||||
navigateWithLastTools(
|
||||
navigateToConvo(
|
||||
cachedConvo ??
|
||||
({
|
||||
conversationId,
|
||||
title,
|
||||
} as TConversation),
|
||||
true,
|
||||
true,
|
||||
{ resetLatestMessage: true },
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -220,6 +220,7 @@ const Conversations: FC<ConversationsProps> = ({
|
||||
role="list"
|
||||
aria-label="Conversations"
|
||||
onRowsRendered={handleRowsRendered}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
|
||||
@@ -31,11 +31,11 @@ export default function Conversation({
|
||||
const params = useParams();
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const { navigateToConvo } = useNavigateToConvo();
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
const currentConvoId = useMemo(() => params.conversationId, [params.conversationId]);
|
||||
const updateConvoMutation = useUpdateConversationMutation(currentConvoId ?? '');
|
||||
const activeConvos = useRecoilValue(store.allConversationsSelector);
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
const { navigateWithLastTools } = useNavigateToConvo();
|
||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
const { conversationId, title = '' } = conversation;
|
||||
|
||||
@@ -118,10 +118,10 @@ export default function Conversation({
|
||||
document.title = title;
|
||||
}
|
||||
|
||||
navigateWithLastTools(
|
||||
conversation,
|
||||
!(conversationId ?? '') || conversationId === Constants.NEW_CONVO,
|
||||
);
|
||||
navigateToConvo(conversation, {
|
||||
currentConvoId,
|
||||
resetLatestMessage: !(conversationId ?? '') || conversationId === Constants.NEW_CONVO,
|
||||
});
|
||||
};
|
||||
|
||||
const convoOptionsProps = {
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import * as Ariakit from '@ariakit/react';
|
||||
import { VisuallyHidden } from '@ariakit/react';
|
||||
import { GitFork, InfoIcon } from 'lucide-react';
|
||||
import * as Popover from '@radix-ui/react-popover';
|
||||
import { ForkOptions } from 'librechat-data-provider';
|
||||
import { GitCommit, GitBranchPlus, ListTree } from 'lucide-react';
|
||||
import {
|
||||
Checkbox,
|
||||
HoverCard,
|
||||
HoverCardTrigger,
|
||||
HoverCardPortal,
|
||||
HoverCardContent,
|
||||
} from '~/components/ui';
|
||||
import OptionHover from '~/components/SidePanel/Parameters/OptionHover';
|
||||
import { TranslationKeys, useLocalize, useNavigateToConvo } from '~/hooks';
|
||||
import { useForkConvoMutation } from '~/data-provider';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { ESide } from '~/common';
|
||||
import { cn } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
@@ -24,11 +16,11 @@ interface PopoverButtonProps {
|
||||
setting: ForkOptions;
|
||||
onClick: (setting: ForkOptions) => void;
|
||||
setActiveSetting: React.Dispatch<React.SetStateAction<TranslationKeys>>;
|
||||
sideOffset?: number;
|
||||
timeoutRef: React.MutableRefObject<NodeJS.Timeout | null>;
|
||||
hoverInfo?: React.ReactNode | string;
|
||||
hoverTitle?: React.ReactNode | string;
|
||||
hoverDescription?: React.ReactNode | string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const optionLabels: Record<ForkOptions, TranslationKeys> = {
|
||||
@@ -38,57 +30,83 @@ const optionLabels: Record<ForkOptions, TranslationKeys> = {
|
||||
[ForkOptions.DEFAULT]: 'com_ui_fork_from_message',
|
||||
};
|
||||
|
||||
const chevronDown = (
|
||||
<svg width="1em" height="1em" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const PopoverButton: React.FC<PopoverButtonProps> = ({
|
||||
children,
|
||||
setting,
|
||||
onClick,
|
||||
setActiveSetting,
|
||||
sideOffset = 30,
|
||||
timeoutRef,
|
||||
hoverInfo,
|
||||
hoverTitle,
|
||||
hoverDescription,
|
||||
label,
|
||||
}) => {
|
||||
const localize = useLocalize();
|
||||
return (
|
||||
<HoverCard openDelay={200}>
|
||||
<Popover.Close
|
||||
onClick={() => onClick(setting)}
|
||||
onMouseEnter={() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
<Ariakit.HovercardProvider>
|
||||
<div className="flex flex-col items-center">
|
||||
<Ariakit.HovercardAnchor
|
||||
render={
|
||||
<Ariakit.Button
|
||||
onClick={() => onClick(setting)}
|
||||
onMouseEnter={() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
setActiveSetting(optionLabels[setting]);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setActiveSetting(optionLabels[ForkOptions.DEFAULT]);
|
||||
}, 175);
|
||||
}}
|
||||
className="mx-1 max-w-14 flex-1 rounded-lg border-2 border-border-medium bg-surface-secondary text-text-secondary transition duration-300 ease-in-out hover:border-border-xheavy hover:bg-surface-hover hover:text-text-primary"
|
||||
aria-label={label}
|
||||
>
|
||||
{children}
|
||||
<VisuallyHidden>{label}</VisuallyHidden>
|
||||
</Ariakit.Button>
|
||||
}
|
||||
setActiveSetting(optionLabels[setting]);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setActiveSetting(optionLabels[ForkOptions.DEFAULT]);
|
||||
}, 175);
|
||||
}}
|
||||
className="mx-1 max-w-14 flex-1 rounded-lg border-2 bg-white text-gray-700 transition duration-300 ease-in-out hover:bg-gray-200 hover:text-gray-900 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600 dark:hover:text-gray-100"
|
||||
type="button"
|
||||
>
|
||||
{children}
|
||||
</Popover.Close>
|
||||
{((hoverInfo != null && hoverInfo !== '') ||
|
||||
(hoverTitle != null && hoverTitle !== '') ||
|
||||
(hoverDescription != null && hoverDescription !== '')) && (
|
||||
<HoverCardPortal>
|
||||
<HoverCardContent side="right" className="z-[999] w-80 dark:bg-gray-700" sideOffset={sideOffset}>
|
||||
/>
|
||||
<Ariakit.HovercardDisclosure className="rounded-full text-text-secondary focus:outline-none focus:ring-2 focus:ring-ring">
|
||||
<VisuallyHidden>
|
||||
{localize('com_ui_fork_more_details_about', { 0: label })}
|
||||
</VisuallyHidden>
|
||||
{chevronDown}
|
||||
</Ariakit.HovercardDisclosure>
|
||||
{((hoverInfo != null && hoverInfo !== '') ||
|
||||
(hoverTitle != null && hoverTitle !== '') ||
|
||||
(hoverDescription != null && hoverDescription !== '')) && (
|
||||
<Ariakit.Hovercard
|
||||
gutter={16}
|
||||
className="z-[999] w-80 rounded-md border border-border-medium bg-surface-secondary p-4 text-text-primary shadow-md"
|
||||
portal={true}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<p className="flex flex-col gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<p className="flex flex-col gap-2 text-sm text-text-secondary">
|
||||
{hoverInfo && hoverInfo}
|
||||
{hoverTitle && <span className="flex flex-wrap gap-1 font-bold">{hoverTitle}</span>}
|
||||
{hoverDescription && hoverDescription}
|
||||
</p>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
)}
|
||||
</HoverCard>
|
||||
</Ariakit.Hovercard>
|
||||
)}
|
||||
</div>
|
||||
</Ariakit.HovercardProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -114,6 +132,9 @@ export default function Fork({
|
||||
const [activeSetting, setActiveSetting] = useState(optionLabels.default);
|
||||
const [splitAtTarget, setSplitAtTarget] = useRecoilState(store.splitAtTarget);
|
||||
const [rememberGlobal, setRememberGlobal] = useRecoilState(store.rememberDefaultFork);
|
||||
const popoverStore = Ariakit.usePopoverStore({
|
||||
placement: 'top',
|
||||
});
|
||||
const forkConvo = useForkConvoMutation({
|
||||
onSuccess: (data) => {
|
||||
navigateToConvo(data.conversation);
|
||||
@@ -157,12 +178,12 @@ export default function Fork({
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover.Root>
|
||||
<Popover.Trigger asChild>
|
||||
<>
|
||||
<Ariakit.PopoverAnchor store={popoverStore}>
|
||||
<button
|
||||
className={cn(
|
||||
'hover-button active rounded-md p-1 text-gray-500 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible ',
|
||||
'data-[state=open]:active focus:opacity-100 data-[state=open]:bg-gray-100 data-[state=open]:text-gray-500 data-[state=open]:dark:bg-gray-700 data-[state=open]:dark:text-gray-200',
|
||||
'hover-button active rounded-md p-1 text-gray-500 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:invisible md:group-hover:visible',
|
||||
'data-[state=open]:active focus:opacity-100 data-[state=open]:bg-gray-100 data-[state=open]:text-gray-500 data-[state=open]:dark:bg-gray-700 data-[state=open]:dark:text-gray-200',
|
||||
!isLast ? 'data-[state=open]:opacity-100 md:opacity-0 md:group-hover:opacity-100' : '',
|
||||
)}
|
||||
onClick={(e) => {
|
||||
@@ -175,155 +196,203 @@ export default function Fork({
|
||||
option: forkSetting,
|
||||
latestMessageId,
|
||||
});
|
||||
} else {
|
||||
popoverStore.toggle();
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
title={localize('com_ui_fork')}
|
||||
aria-label={localize('com_ui_fork')}
|
||||
>
|
||||
<GitFork className="h-4 w-4 hover:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400" />
|
||||
</button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<div dir="ltr">
|
||||
<Popover.Content
|
||||
side="top"
|
||||
role="menu"
|
||||
className="bg-token-surface-primary flex min-h-[120px] min-w-[215px] flex-col gap-3 overflow-hidden rounded-lg bg-white p-2 px-3 shadow-lg dark:bg-gray-700"
|
||||
style={{ outline: 'none', pointerEvents: 'auto', boxSizing: 'border-box' }}
|
||||
tabIndex={-1}
|
||||
sideOffset={5}
|
||||
align="center"
|
||||
>
|
||||
<div className="flex h-6 w-full items-center justify-center text-sm dark:text-gray-200">
|
||||
{localize(activeSetting )}
|
||||
<HoverCard openDelay={50}>
|
||||
<HoverCardTrigger asChild>
|
||||
<InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-gray-500 dark:text-white/50" />
|
||||
</HoverCardTrigger>
|
||||
<HoverCardPortal>
|
||||
<HoverCardContent
|
||||
side="right"
|
||||
className="z-[999] w-80 dark:bg-gray-700"
|
||||
sideOffset={19}
|
||||
</Ariakit.PopoverAnchor>
|
||||
<Ariakit.Popover
|
||||
store={popoverStore}
|
||||
gutter={5}
|
||||
className="flex min-h-[120px] min-w-[215px] flex-col gap-3 overflow-hidden rounded-lg border border-border-heavy bg-surface-secondary p-2 px-3 shadow-lg"
|
||||
style={{
|
||||
outline: 'none',
|
||||
pointerEvents: 'auto',
|
||||
zIndex: 50,
|
||||
}}
|
||||
portal={true}
|
||||
>
|
||||
<div className="flex h-8 w-full items-center justify-center text-sm text-text-primary">
|
||||
{localize(activeSetting)}
|
||||
<Ariakit.HovercardProvider>
|
||||
<div className="ml-auto flex h-6 w-6 items-center justify-center gap-1">
|
||||
<Ariakit.HovercardAnchor
|
||||
render={
|
||||
<button
|
||||
className="flex h-5 w-5 items-center rounded-full text-text-secondary"
|
||||
aria-label={localize('com_ui_fork_info_button_label')}
|
||||
>
|
||||
<div className="flex flex-col gap-2 space-y-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<span>{localize('com_ui_fork_info_1')}</span>
|
||||
<span>{localize('com_ui_fork_info_2')}</span>
|
||||
<span>
|
||||
{localize('com_ui_fork_info_3', {
|
||||
0: localize('com_ui_fork_split_target'),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
</HoverCard>
|
||||
</div>
|
||||
<div className="flex h-full w-full items-center justify-center gap-1">
|
||||
<PopoverButton
|
||||
sideOffset={155}
|
||||
setActiveSetting={setActiveSetting}
|
||||
timeoutRef={timeoutRef}
|
||||
onClick={onClick}
|
||||
setting={ForkOptions.DIRECT_PATH}
|
||||
hoverTitle={
|
||||
<>
|
||||
<GitCommit className="h-5 w-5 rotate-90" />
|
||||
{localize(optionLabels[ForkOptions.DIRECT_PATH])}
|
||||
</>
|
||||
<InfoIcon />
|
||||
</button>
|
||||
}
|
||||
hoverDescription={localize('com_ui_fork_info_visible')}
|
||||
>
|
||||
<HoverCardTrigger asChild>
|
||||
<GitCommit className="h-full w-full rotate-90 p-2" />
|
||||
</HoverCardTrigger>
|
||||
</PopoverButton>
|
||||
<PopoverButton
|
||||
sideOffset={90}
|
||||
setActiveSetting={setActiveSetting}
|
||||
timeoutRef={timeoutRef}
|
||||
onClick={onClick}
|
||||
setting={ForkOptions.INCLUDE_BRANCHES}
|
||||
hoverTitle={
|
||||
<>
|
||||
<GitBranchPlus className="h-4 w-4 rotate-180" />
|
||||
{localize(optionLabels[ForkOptions.INCLUDE_BRANCHES])}
|
||||
</>
|
||||
}
|
||||
hoverDescription={localize('com_ui_fork_info_branches')}
|
||||
>
|
||||
<HoverCardTrigger asChild>
|
||||
<GitBranchPlus className="h-full w-full rotate-180 p-2" />
|
||||
</HoverCardTrigger>
|
||||
</PopoverButton>
|
||||
<PopoverButton
|
||||
sideOffset={25}
|
||||
setActiveSetting={setActiveSetting}
|
||||
timeoutRef={timeoutRef}
|
||||
onClick={onClick}
|
||||
setting={ForkOptions.TARGET_LEVEL}
|
||||
hoverTitle={
|
||||
<>
|
||||
<ListTree className="h-5 w-5" />
|
||||
{`${localize(
|
||||
optionLabels[ForkOptions.TARGET_LEVEL],
|
||||
)} (${localize('com_endpoint_default')})`}
|
||||
</>
|
||||
}
|
||||
hoverDescription={localize('com_ui_fork_info_target')}
|
||||
>
|
||||
<HoverCardTrigger asChild>
|
||||
<ListTree className="h-full w-full p-2" />
|
||||
</HoverCardTrigger>
|
||||
</PopoverButton>
|
||||
</div>
|
||||
<HoverCard openDelay={50}>
|
||||
<HoverCardTrigger asChild>
|
||||
<div className="flex h-6 w-full items-center justify-start text-sm dark:text-gray-300 dark:hover:text-gray-200">
|
||||
<Checkbox
|
||||
checked={splitAtTarget}
|
||||
onCheckedChange={(checked: boolean) => setSplitAtTarget(checked)}
|
||||
className="m-2 transition duration-300 ease-in-out"
|
||||
/>
|
||||
{localize('com_ui_fork_split_target')}
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover
|
||||
side={ESide.Right}
|
||||
description="com_ui_fork_info_start"
|
||||
langCode={true}
|
||||
sideOffset={20}
|
||||
/>
|
||||
</HoverCard>
|
||||
<HoverCard openDelay={50}>
|
||||
<HoverCardTrigger asChild>
|
||||
<div className="flex h-6 w-full items-center justify-start text-sm dark:text-gray-300 dark:hover:text-gray-200">
|
||||
<Checkbox
|
||||
<Ariakit.HovercardDisclosure className="rounded-full text-text-secondary focus:outline-none focus:ring-2 focus:ring-ring">
|
||||
<VisuallyHidden>{localize('com_ui_fork_more_info_options')}</VisuallyHidden>
|
||||
{chevronDown}
|
||||
</Ariakit.HovercardDisclosure>
|
||||
</div>
|
||||
<Ariakit.Hovercard
|
||||
gutter={19}
|
||||
className="z-[999] w-80 rounded-md border border-border-medium bg-surface-secondary p-4 text-text-primary shadow-md"
|
||||
portal={true}
|
||||
>
|
||||
<div className="flex flex-col gap-2 space-y-2 text-sm text-text-secondary">
|
||||
<span>{localize('com_ui_fork_info_1')}</span>
|
||||
<span>{localize('com_ui_fork_info_2')}</span>
|
||||
<span>
|
||||
{localize('com_ui_fork_info_3', {
|
||||
0: localize('com_ui_fork_split_target'),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</Ariakit.Hovercard>
|
||||
</Ariakit.HovercardProvider>
|
||||
</div>
|
||||
<div className="flex h-full w-full items-center justify-center gap-1">
|
||||
<PopoverButton
|
||||
setActiveSetting={setActiveSetting}
|
||||
timeoutRef={timeoutRef}
|
||||
onClick={onClick}
|
||||
setting={ForkOptions.DIRECT_PATH}
|
||||
label={localize(optionLabels[ForkOptions.DIRECT_PATH])}
|
||||
hoverTitle={
|
||||
<>
|
||||
<GitCommit className="h-5 w-5 rotate-90" />
|
||||
{localize(optionLabels[ForkOptions.DIRECT_PATH])}
|
||||
</>
|
||||
}
|
||||
hoverDescription={localize('com_ui_fork_info_visible')}
|
||||
>
|
||||
<GitCommit className="h-full w-full rotate-90 p-2" />
|
||||
</PopoverButton>
|
||||
<PopoverButton
|
||||
setActiveSetting={setActiveSetting}
|
||||
timeoutRef={timeoutRef}
|
||||
onClick={onClick}
|
||||
setting={ForkOptions.INCLUDE_BRANCHES}
|
||||
label={localize(optionLabels[ForkOptions.INCLUDE_BRANCHES])}
|
||||
hoverTitle={
|
||||
<>
|
||||
<GitBranchPlus className="h-4 w-4 rotate-180" />
|
||||
{localize(optionLabels[ForkOptions.INCLUDE_BRANCHES])}
|
||||
</>
|
||||
}
|
||||
hoverDescription={localize('com_ui_fork_info_branches')}
|
||||
>
|
||||
<GitBranchPlus className="h-full w-full rotate-180 p-2" />
|
||||
</PopoverButton>
|
||||
<PopoverButton
|
||||
setActiveSetting={setActiveSetting}
|
||||
timeoutRef={timeoutRef}
|
||||
onClick={onClick}
|
||||
setting={ForkOptions.TARGET_LEVEL}
|
||||
label={localize(optionLabels[ForkOptions.TARGET_LEVEL])}
|
||||
hoverTitle={
|
||||
<>
|
||||
<ListTree className="h-5 w-5" />
|
||||
{`${localize(
|
||||
optionLabels[ForkOptions.TARGET_LEVEL],
|
||||
)} (${localize('com_endpoint_default')})`}
|
||||
</>
|
||||
}
|
||||
hoverDescription={localize('com_ui_fork_info_target')}
|
||||
>
|
||||
<ListTree className="h-full w-full p-2" />
|
||||
</PopoverButton>
|
||||
</div>
|
||||
<Ariakit.HovercardProvider>
|
||||
<div className="flex items-center">
|
||||
<Ariakit.HovercardAnchor
|
||||
render={
|
||||
<div className="flex h-6 w-full select-none items-center justify-start rounded-md text-sm text-text-secondary hover:text-text-primary">
|
||||
<Ariakit.Checkbox
|
||||
id="split-target-checkbox"
|
||||
checked={splitAtTarget}
|
||||
onChange={(event) => setSplitAtTarget(event.target.checked)}
|
||||
className="m-2 h-4 w-4 rounded-sm border border-primary ring-offset-background transition duration-300 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
|
||||
aria-label={localize('com_ui_fork_split_target')}
|
||||
/>
|
||||
<label htmlFor="split-target-checkbox" className="ml-2 cursor-pointer">
|
||||
{localize('com_ui_fork_split_target')}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Ariakit.HovercardDisclosure className="rounded-full text-text-secondary focus:outline-none focus:ring-2 focus:ring-ring">
|
||||
<VisuallyHidden>
|
||||
{localize('com_ui_fork_more_info_split_target', {
|
||||
0: localize('com_ui_fork_split_target'),
|
||||
})}
|
||||
</VisuallyHidden>
|
||||
{chevronDown}
|
||||
</Ariakit.HovercardDisclosure>
|
||||
</div>
|
||||
<Ariakit.Hovercard
|
||||
gutter={32}
|
||||
className="z-[999] w-80 select-none rounded-md border border-border-medium bg-surface-secondary p-4 text-text-primary shadow-md"
|
||||
portal={true}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-text-secondary">{localize('com_ui_fork_info_start')}</p>
|
||||
</div>
|
||||
</Ariakit.Hovercard>
|
||||
</Ariakit.HovercardProvider>
|
||||
<Ariakit.HovercardProvider>
|
||||
<div className="flex items-center">
|
||||
<Ariakit.HovercardAnchor
|
||||
render={
|
||||
<div
|
||||
onClick={() => setRemember((prev) => !prev)}
|
||||
className="flex h-6 w-full select-none items-center justify-start rounded-md text-sm text-text-secondary hover:text-text-primary"
|
||||
>
|
||||
<Ariakit.Checkbox
|
||||
id="remember-checkbox"
|
||||
checked={remember}
|
||||
onCheckedChange={(checked: boolean) => {
|
||||
onChange={(event) => {
|
||||
const checked = event.target.checked;
|
||||
console.log('checked', checked);
|
||||
if (checked) {
|
||||
showToast({
|
||||
message: localize('com_ui_fork_remember_checked'),
|
||||
status: 'info',
|
||||
});
|
||||
}
|
||||
setRemember(checked);
|
||||
return setRemember(checked);
|
||||
}}
|
||||
className="m-2 transition duration-300 ease-in-out"
|
||||
className="m-2 h-4 w-4 rounded-sm border border-primary ring-offset-background transition duration-300 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
|
||||
aria-label={localize('com_ui_fork_remember')}
|
||||
/>
|
||||
{localize('com_ui_fork_remember')}
|
||||
<label htmlFor="remember-checkbox" className="ml-2 cursor-pointer">
|
||||
{localize('com_ui_fork_remember')}
|
||||
</label>
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<OptionHover
|
||||
side={ESide.Right}
|
||||
description="com_ui_fork_info_remember"
|
||||
langCode={true}
|
||||
sideOffset={20}
|
||||
/>
|
||||
</HoverCard>
|
||||
</Popover.Content>
|
||||
</div>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
}
|
||||
/>
|
||||
<Ariakit.HovercardDisclosure className="rounded-full text-text-secondary focus:outline-none focus:ring-2 focus:ring-ring">
|
||||
<VisuallyHidden>
|
||||
{localize('com_ui_fork_more_info_remember', {
|
||||
0: localize('com_ui_fork_remember'),
|
||||
})}
|
||||
</VisuallyHidden>
|
||||
{chevronDown}
|
||||
</Ariakit.HovercardDisclosure>
|
||||
</div>
|
||||
<Ariakit.Hovercard
|
||||
gutter={14}
|
||||
className="z-[999] w-80 rounded-md border border-border-medium bg-surface-secondary p-4 text-text-primary shadow-md"
|
||||
portal={true}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-text-secondary">{localize('com_ui_fork_info_remember')}</p>
|
||||
</div>
|
||||
</Ariakit.Hovercard>
|
||||
</Ariakit.HovercardProvider>
|
||||
</Ariakit.Popover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ export default function MobileNav({
|
||||
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
|
||||
[],
|
||||
);
|
||||
queryClient.invalidateQueries([QueryKeys.messages]);
|
||||
newConversation();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -40,8 +40,9 @@ export default function NewChat({
|
||||
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
|
||||
[],
|
||||
);
|
||||
queryClient.invalidateQueries([QueryKeys.messages]);
|
||||
newConvo();
|
||||
navigate('/c/new');
|
||||
navigate('/c/new', { state: { focusChat: true } });
|
||||
if (isSmallScreen) {
|
||||
toggleNav();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { forwardRef, useState, useCallback, useMemo, useEffect, Ref } from 'react';
|
||||
import React, { forwardRef, useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { Search, X } from 'lucide-react';
|
||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||
import { QueryKeys } from 'librechat-data-provider';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
@@ -13,7 +13,7 @@ type SearchBarProps = {
|
||||
isSmallScreen?: boolean;
|
||||
};
|
||||
|
||||
const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) => {
|
||||
const SearchBar = forwardRef((props: SearchBarProps, ref: React.Ref<HTMLDivElement>) => {
|
||||
const localize = useLocalize();
|
||||
const location = useLocation();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -21,11 +21,11 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
||||
const { isSmallScreen } = props;
|
||||
|
||||
const [text, setText] = useState('');
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [showClearIcon, setShowClearIcon] = useState(false);
|
||||
|
||||
const { newConversation } = useNewConvo();
|
||||
const setSearchState = useSetRecoilState(store.search);
|
||||
const search = useRecoilValue(store.search);
|
||||
const [search, setSearchState] = useRecoilState(store.search);
|
||||
|
||||
const clearSearch = useCallback(() => {
|
||||
if (location.pathname.includes('/search')) {
|
||||
@@ -44,6 +44,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
||||
isTyping: false,
|
||||
}));
|
||||
clearSearch();
|
||||
inputRef.current?.focus();
|
||||
}, [setSearchState, clearSearch]);
|
||||
|
||||
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
@@ -108,6 +109,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
||||
<Search className="absolute left-3 h-4 w-4 text-text-secondary group-focus-within:text-text-primary group-hover:text-text-primary" />
|
||||
<input
|
||||
type="text"
|
||||
ref={inputRef}
|
||||
className="m-0 mr-0 w-full border-none bg-transparent p-0 pl-7 text-sm leading-tight placeholder-text-secondary placeholder-opacity-100 focus-visible:outline-none group-focus-within:placeholder-text-primary group-hover:placeholder-text-primary"
|
||||
value={text}
|
||||
onChange={onChange}
|
||||
@@ -122,14 +124,20 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
||||
autoComplete="off"
|
||||
dir="auto"
|
||||
/>
|
||||
<X
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`${localize('com_ui_clear')} ${localize('com_ui_search')}`}
|
||||
className={cn(
|
||||
'absolute right-[7px] h-5 w-5 cursor-pointer transition-opacity duration-200',
|
||||
'absolute right-[7px] flex h-5 w-5 items-center justify-center rounded-full border-none bg-transparent p-0 transition-opacity duration-200',
|
||||
showClearIcon ? 'opacity-100' : 'opacity-0',
|
||||
isSmallScreen === true ? 'right-[16px]' : '',
|
||||
)}
|
||||
onClick={clearText}
|
||||
/>
|
||||
tabIndex={showClearIcon ? 0 : -1}
|
||||
disabled={!showClearIcon}
|
||||
>
|
||||
<X className="h-5 w-5 cursor-pointer" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -44,6 +44,7 @@ export const ForkSettings = () => {
|
||||
options={forkOptions}
|
||||
sizeClasses="w-[200px]"
|
||||
testId="fork-setting-dropdown"
|
||||
className="z-[50]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -102,8 +102,8 @@ export default function LanguageSTTDropdown() {
|
||||
onChange={handleSelect}
|
||||
options={languageOptions}
|
||||
sizeClasses="[--anchor-max-height:256px]"
|
||||
anchor="bottom start"
|
||||
testId="LanguageSTTDropdown"
|
||||
className="z-50"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -28,9 +28,9 @@ const categoryColorMap: Record<string, string> = {
|
||||
code: 'text-red-500',
|
||||
misc: 'text-blue-300',
|
||||
shop: 'text-purple-400',
|
||||
idea: 'text-yellow-300',
|
||||
idea: 'text-yellow-500/90 dark:text-yellow-300 ',
|
||||
write: 'text-purple-400',
|
||||
travel: 'text-yellow-300',
|
||||
travel: 'text-yellow-500/90 dark:text-yellow-300 ',
|
||||
finance: 'text-orange-400',
|
||||
roleplay: 'text-orange-400',
|
||||
teach_or_explain: 'text-blue-300',
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useFormContext, Controller } from 'react-hook-form';
|
||||
import { LocalStorageKeys } from 'librechat-data-provider';
|
||||
import { Dropdown } from '~/components/ui';
|
||||
@@ -15,6 +17,7 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
|
||||
onValueChange,
|
||||
className = '',
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const formContext = useFormContext();
|
||||
const { categories, emptyCategory } = useCategories();
|
||||
|
||||
@@ -32,13 +35,25 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
|
||||
[watchedCategory, categories, currentCategory, emptyCategory],
|
||||
);
|
||||
|
||||
const displayCategory = useMemo(() => {
|
||||
if (!categoryOption.value && !('icon' in categoryOption)) {
|
||||
return {
|
||||
...categoryOption,
|
||||
icon: (<span className="i-heroicons-tag" />) as ReactNode,
|
||||
label: categoryOption.label || t('com_ui_empty_category'),
|
||||
};
|
||||
}
|
||||
return categoryOption;
|
||||
}, [categoryOption, t]);
|
||||
|
||||
return formContext ? (
|
||||
<Controller
|
||||
name="category"
|
||||
control={control}
|
||||
render={() => (
|
||||
<Dropdown
|
||||
value={categoryOption.value ?? ''}
|
||||
value={displayCategory.value ?? ''}
|
||||
label={displayCategory.value ? undefined : t('com_ui_category')}
|
||||
onChange={(value: string) => {
|
||||
setValue('category', value, { shouldDirty: false });
|
||||
localStorage.setItem(LocalStorageKeys.LAST_PROMPT_CATEGORY, value);
|
||||
@@ -48,10 +63,12 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
|
||||
ariaLabel="Prompt's category selector"
|
||||
className={className}
|
||||
options={categories || []}
|
||||
renderValue={(option) => (
|
||||
renderValue={() => (
|
||||
<div className="flex items-center space-x-2">
|
||||
{option.icon != null && <span>{option.icon as React.ReactNode}</span>}
|
||||
<span>{option.label}</span>
|
||||
{'icon' in displayCategory && displayCategory.icon != null && (
|
||||
<span>{displayCategory.icon as ReactNode}</span>
|
||||
)}
|
||||
<span>{displayCategory.label}</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
@@ -68,10 +85,12 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
|
||||
ariaLabel="Prompt's category selector"
|
||||
className={className}
|
||||
options={categories || []}
|
||||
renderValue={(option) => (
|
||||
renderValue={() => (
|
||||
<div className="flex items-center space-x-2">
|
||||
{option.icon != null && <span>{option.icon as React.ReactNode}</span>}
|
||||
<span>{option.label}</span>
|
||||
{'icon' in displayCategory && displayCategory.icon != null && (
|
||||
<span>{displayCategory.icon as ReactNode}</span>
|
||||
)}
|
||||
<span>{displayCategory.label}</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -57,7 +57,7 @@ function ChatGroupItem({
|
||||
snippet={
|
||||
typeof group.oneliner === 'string' && group.oneliner.length > 0
|
||||
? group.oneliner
|
||||
: group.productionPrompt?.prompt ?? ''
|
||||
: (group.productionPrompt?.prompt ?? '')
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
@@ -83,7 +83,11 @@ function ChatGroupItem({
|
||||
className="z-50 inline-flex h-8 w-8 items-center justify-center rounded-lg border border-border-medium bg-transparent p-0 text-sm font-medium transition-all duration-300 ease-in-out hover:border-border-heavy hover:bg-surface-hover focus:border-border-heavy focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
<MenuIcon className="icon-md text-text-secondary" aria-hidden="true" />
|
||||
<span className="sr-only">Open actions menu for {group.name}</span>
|
||||
<span className="sr-only">
|
||||
{localize('com_ui_sr_actions_menu', { 0: group.name }) +
|
||||
' ' +
|
||||
localize('com_ui_prompt')}
|
||||
</span>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
|
||||
@@ -7,6 +7,7 @@ import PromptVariables from '~/components/Prompts/PromptVariables';
|
||||
import { Button, TextareaAutosize, Input } from '~/components/ui';
|
||||
import Description from '~/components/Prompts/Description';
|
||||
import { useLocalize, useHasAccess } from '~/hooks';
|
||||
import VariablesDropdown from '~/components/Prompts/VariablesDropdown';
|
||||
import Command from '~/components/Prompts/Command';
|
||||
import { useCreatePrompt } from '~/data-provider';
|
||||
import { cn } from '~/utils';
|
||||
@@ -132,7 +133,8 @@ const CreatePromptForm = ({
|
||||
<div className="flex w-full flex-col gap-4 md:mt-[1.075rem]">
|
||||
<div>
|
||||
<h2 className="flex items-center justify-between rounded-t-lg border border-border-medium py-2 pl-4 pr-1 text-base font-semibold dark:text-gray-200">
|
||||
{localize('com_ui_prompt_text')}*
|
||||
<span>{localize('com_ui_prompt_text')}*</span>
|
||||
<VariablesDropdown fieldName="prompt" className="mr-2" />
|
||||
</h2>
|
||||
<div className="min-h-32 rounded-b-lg border border-border-medium p-4 transition-all duration-150">
|
||||
<Controller
|
||||
|
||||
@@ -31,7 +31,7 @@ const VariableDialog: React.FC<VariableDialogProps> = ({ open, onClose, group })
|
||||
|
||||
return (
|
||||
<OGDialog open={open} onOpenChange={handleOpenChange}>
|
||||
<OGDialogContent className="max-w-full bg-white dark:border-gray-700 dark:bg-gray-850 dark:text-gray-300 md:max-w-3xl">
|
||||
<OGDialogContent className="max-h-[90vh] max-w-full overflow-y-auto bg-white dark:border-gray-700 dark:bg-gray-850 dark:text-gray-300 md:max-w-[60vw]">
|
||||
<OGDialogTitle>{group.name}</OGDialogTitle>
|
||||
<VariableForm group={group} onClose={onClose} />
|
||||
</OGDialogContent>
|
||||
|
||||
@@ -5,18 +5,14 @@ import supersub from 'remark-supersub';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import { replaceSpecialVars } from 'librechat-data-provider';
|
||||
import { useForm, useFieldArray, Controller, useWatch } from 'react-hook-form';
|
||||
import type { TPromptGroup } from 'librechat-data-provider';
|
||||
import {
|
||||
cn,
|
||||
wrapVariable,
|
||||
defaultTextProps,
|
||||
replaceSpecialVars,
|
||||
extractVariableInfo,
|
||||
} from '~/utils';
|
||||
import { cn, wrapVariable, defaultTextProps, extractVariableInfo } from '~/utils';
|
||||
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
|
||||
import { TextareaAutosize, InputCombobox, Button } from '~/components/ui';
|
||||
import { useAuthContext, useLocalize, useSubmitMessage } from '~/hooks';
|
||||
import { PromptVariableGfm } from '../Markdown';
|
||||
|
||||
type FieldType = 'text' | 'select';
|
||||
|
||||
@@ -115,9 +111,12 @@ export default function VariableForm({
|
||||
allVariables.forEach((variable) => {
|
||||
const placeholder = `{{${variable}}}`;
|
||||
const fieldIndex = variableIndexMap.get(variable) as string | number;
|
||||
const fieldValue = fieldValues[fieldIndex].value as string;
|
||||
const highlightText = fieldValue !== '' ? fieldValue : placeholder;
|
||||
tempText = tempText.replaceAll(placeholder, `**${highlightText}**`);
|
||||
const fieldValue = fieldValues[fieldIndex].value as string | undefined;
|
||||
if (fieldValue === placeholder || fieldValue === '' || !fieldValue) {
|
||||
return;
|
||||
}
|
||||
const highlightText = fieldValue !== '' ? `**${fieldValue}**` : placeholder;
|
||||
tempText = tempText.replaceAll(placeholder, highlightText);
|
||||
});
|
||||
return tempText;
|
||||
};
|
||||
@@ -141,7 +140,7 @@ export default function VariableForm({
|
||||
return (
|
||||
<div className="mx-auto p-1 md:container">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="mb-6 max-h-screen max-w-[90vw] overflow-auto rounded-md bg-gray-100 p-4 text-text-secondary dark:bg-gray-700/50 sm:max-w-full md:max-h-80">
|
||||
<div className="mb-6 max-h-screen max-w-[90vw] overflow-auto rounded-md bg-surface-tertiary p-4 text-text-secondary dark:bg-surface-primary sm:max-w-full md:max-h-96">
|
||||
<ReactMarkdown
|
||||
/** @ts-ignore */
|
||||
remarkPlugins={[supersub, remarkGfm, [remarkMath, { singleDollarTextMath: true }]]}
|
||||
@@ -152,8 +151,8 @@ export default function VariableForm({
|
||||
[rehypeHighlight, { ignoreMissing: true }],
|
||||
]}
|
||||
/** @ts-ignore */
|
||||
components={{ code: codeNoExecution }}
|
||||
className="prose dark:prose-invert light dark:text-gray-70 my-1 max-h-[50vh] break-words"
|
||||
components={{ code: codeNoExecution, p: PromptVariableGfm }}
|
||||
className="markdown prose dark:prose-invert light my-1 max-h-[50vh] max-w-full break-words dark:text-text-secondary"
|
||||
>
|
||||
{generateHighlightedMarkdown()}
|
||||
</ReactMarkdown>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { handleDoubleClick } from '~/utils';
|
||||
|
||||
export const CodeVariableGfm = ({ children }: { children: React.ReactNode }) => {
|
||||
export const CodeVariableGfm: React.ElementType = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<code
|
||||
onDoubleClick={handleDoubleClick}
|
||||
@@ -29,7 +29,10 @@ export const PromptVariableGfm = ({
|
||||
const parts = child.split(regex);
|
||||
return parts.map((part, index) =>
|
||||
index % 2 === 1 ? (
|
||||
<b key={index} className="rounded-md bg-yellow-100/90 p-1 text-gray-700">
|
||||
<b
|
||||
key={index}
|
||||
className="ml-[0.5] rounded-lg bg-amber-100 p-[1px] font-medium text-yellow-800 dark:border-yellow-500/50 dark:bg-transparent dark:text-yellow-500/90"
|
||||
>
|
||||
{`{{${part}}}`}
|
||||
</b>
|
||||
) : (
|
||||
|
||||
@@ -13,7 +13,7 @@ const PreviewPrompt = ({
|
||||
}) => {
|
||||
return (
|
||||
<OGDialog open={open} onOpenChange={onOpenChange}>
|
||||
<OGDialogContent className="w-11/12 max-w-5xl">
|
||||
<OGDialogContent className="max-h-[90vh] w-11/12 max-w-full overflow-y-auto md:max-w-[60vw]">
|
||||
<div className="p-2">
|
||||
<PromptDetails group={group} />
|
||||
</div>
|
||||
|
||||
@@ -5,13 +5,13 @@ import rehypeKatex from 'rehype-katex';
|
||||
import remarkMath from 'remark-math';
|
||||
import supersub from 'remark-supersub';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import { replaceSpecialVars } from 'librechat-data-provider';
|
||||
import type { TPromptGroup } from 'librechat-data-provider';
|
||||
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
|
||||
import { useLocalize, useAuthContext } from '~/hooks';
|
||||
import CategoryIcon from './Groups/CategoryIcon';
|
||||
import PromptVariables from './PromptVariables';
|
||||
import { PromptVariableGfm } from './Markdown';
|
||||
import { replaceSpecialVars } from '~/utils';
|
||||
import { Label } from '~/components/ui';
|
||||
import Description from './Description';
|
||||
import Command from './Command';
|
||||
@@ -46,7 +46,7 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
|
||||
<div className="flex h-full max-h-screen flex-col overflow-y-auto md:flex-row">
|
||||
<div className="flex flex-1 flex-col gap-4 p-0 md:max-h-[calc(100vh-150px)] md:p-2">
|
||||
<div>
|
||||
<h2 className="flex items-center justify-between rounded-t-lg border border-border-light py-2 pl-4 text-base font-semibold text-text-primary ">
|
||||
<h2 className="flex items-center justify-between rounded-t-lg border border-border-light py-2 pl-4 text-base font-semibold text-text-primary">
|
||||
{localize('com_ui_prompt_text')}
|
||||
</h2>
|
||||
<div className="group relative min-h-32 rounded-b-lg border border-border-light p-4 transition-all duration-150">
|
||||
@@ -65,7 +65,7 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
|
||||
]}
|
||||
/** @ts-ignore */
|
||||
components={{ p: PromptVariableGfm, code: codeNoExecution }}
|
||||
className="prose dark:prose-invert light dark:text-gray-70 my-1"
|
||||
className="markdown prose dark:prose-invert light dark:text-gray-70 my-1 break-words"
|
||||
>
|
||||
{mainText}
|
||||
</ReactMarkdown>
|
||||
|
||||
@@ -12,6 +12,7 @@ import ReactMarkdown from 'react-markdown';
|
||||
import { codeNoExecution } from '~/components/Chat/Messages/Content/Markdown';
|
||||
import AlwaysMakeProd from '~/components/Prompts/Groups/AlwaysMakeProd';
|
||||
import { SaveIcon, CrossIcon } from '~/components/svg';
|
||||
import VariablesDropdown from './VariablesDropdown';
|
||||
import { TextareaAutosize } from '~/components/ui';
|
||||
import { PromptVariableGfm } from './Markdown';
|
||||
import { PromptsEditorMode } from '~/common';
|
||||
@@ -59,10 +60,11 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
||||
<span className="max-w-[200px] truncate sm:max-w-none">
|
||||
{localize('com_ui_prompt_text')}
|
||||
</span>
|
||||
<div className="flex flex-shrink-0 flex-row gap-3 sm:gap-6">
|
||||
<div className="flex flex-shrink-0 flex-row items-center gap-3 sm:gap-6">
|
||||
{editorMode === PromptsEditorMode.ADVANCED && (
|
||||
<AlwaysMakeProd className="hidden sm:flex" />
|
||||
)}
|
||||
<VariablesDropdown fieldName={name} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsEditing((prev) => !prev)}
|
||||
@@ -105,6 +107,7 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
||||
isEditing ? (
|
||||
<TextareaAutosize
|
||||
{...field}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
className="w-full resize-none overflow-y-auto rounded bg-transparent text-sm text-text-primary focus:outline-none sm:text-base"
|
||||
minRows={3}
|
||||
@@ -123,8 +126,8 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
||||
style={{ minHeight: '4.5em', maxHeight: '21em', overflow: 'auto' }}
|
||||
>
|
||||
<ReactMarkdown
|
||||
/** @ts-ignore */
|
||||
remarkPlugins={[
|
||||
/** @ts-ignore */
|
||||
supersub,
|
||||
remarkGfm,
|
||||
[remarkMath, { singleDollarTextMath: true }],
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Variable } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { specialVariables } from 'librechat-data-provider';
|
||||
import { cn, extractUniqueVariables } from '~/utils';
|
||||
import { CodeVariableGfm } from './Markdown';
|
||||
import { Separator } from '~/components/ui';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
const specialVariables = {
|
||||
current_date: true,
|
||||
current_user: true,
|
||||
};
|
||||
|
||||
const specialVariableClasses =
|
||||
'bg-yellow-500/25 text-yellow-600 dark:border-yellow-500/50 dark:bg-transparent dark:text-yellow-500/90';
|
||||
'bg-amber-100 text-yellow-800 border-yellow-600 dark:border-yellow-500/50 dark:bg-transparent dark:text-yellow-500/90';
|
||||
|
||||
const components: {
|
||||
[nodeType: string]: React.ElementType;
|
||||
} = { code: CodeVariableGfm };
|
||||
|
||||
const PromptVariables = ({
|
||||
promptText,
|
||||
@@ -52,7 +52,7 @@ const PromptVariables = ({
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-text-secondary">
|
||||
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
||||
<ReactMarkdown components={components} className="markdown prose dark:prose-invert">
|
||||
{localize('com_ui_variables_info')}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
@@ -65,8 +65,8 @@ const PromptVariables = ({
|
||||
{localize('com_ui_special_variables')}
|
||||
</span>
|
||||
<span className="text-sm text-text-secondary">
|
||||
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
||||
{localize('com_ui_special_variables_info')}
|
||||
<ReactMarkdown components={components} className="markdown prose dark:prose-invert">
|
||||
{localize('com_ui_special_variables_more_info')}
|
||||
</ReactMarkdown>
|
||||
</span>
|
||||
</div>
|
||||
@@ -75,7 +75,7 @@ const PromptVariables = ({
|
||||
{localize('com_ui_dropdown_variables')}
|
||||
</span>
|
||||
<span className="text-sm text-text-secondary">
|
||||
<ReactMarkdown components={{ code: CodeVariableGfm }}>
|
||||
<ReactMarkdown components={components} className="markdown prose dark:prose-invert">
|
||||
{localize('com_ui_dropdown_variables_info')}
|
||||
</ReactMarkdown>
|
||||
</span>
|
||||
|
||||
75
client/src/components/Prompts/VariablesDropdown.tsx
Normal file
75
client/src/components/Prompts/VariablesDropdown.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useState, useId } from 'react';
|
||||
import { PlusCircle } from 'lucide-react';
|
||||
import * as Menu from '@ariakit/react/menu';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { specialVariables } from 'librechat-data-provider';
|
||||
import type { TSpecialVarLabel } from 'librechat-data-provider';
|
||||
import { DropdownPopup } from '~/components';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
interface VariableOption {
|
||||
label: TSpecialVarLabel;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const variableOptions: VariableOption[] = Object.keys(specialVariables).map((key) => ({
|
||||
label: `com_ui_special_var_${key}` as TSpecialVarLabel,
|
||||
value: `{{${key}}}`,
|
||||
}));
|
||||
|
||||
interface VariablesDropdownProps {
|
||||
fieldName?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function VariablesDropdown({
|
||||
fieldName = 'prompt',
|
||||
className = '',
|
||||
}: VariablesDropdownProps) {
|
||||
const menuId = useId();
|
||||
const localize = useLocalize();
|
||||
const methods = useFormContext();
|
||||
const { setValue, getValues } = methods;
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
const handleAddVariable = (label: TSpecialVarLabel, value: string) => {
|
||||
const currentText = getValues(fieldName) || '';
|
||||
const spacer = currentText.length > 0 ? '\n\n' : '';
|
||||
const prefix = localize(label);
|
||||
setValue(fieldName, currentText + spacer + prefix + ': ' + value);
|
||||
setIsMenuOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
title={`${localize('com_ui_add')} ${localize('com_ui_special_variables')}`}
|
||||
>
|
||||
<DropdownPopup
|
||||
portal={true}
|
||||
mountByState={true}
|
||||
unmountOnHide={true}
|
||||
preserveTabOrder={true}
|
||||
isOpen={isMenuOpen}
|
||||
setIsOpen={setIsMenuOpen}
|
||||
trigger={
|
||||
<Menu.MenuButton
|
||||
id="variables-menu-button"
|
||||
aria-label={`${localize('com_ui_add')} ${localize('com_ui_special_variables')}`}
|
||||
className="flex h-8 items-center gap-1 rounded-md border border-border-medium bg-surface-secondary px-2 py-0 text-sm text-text-primary transition-colors duration-200 hover:bg-surface-tertiary"
|
||||
>
|
||||
<PlusCircle className="mr-1 h-3 w-3 text-text-secondary" aria-hidden={true} />
|
||||
{localize('com_ui_special_variables')}
|
||||
</Menu.MenuButton>
|
||||
}
|
||||
items={variableOptions.map((option) => ({
|
||||
label: localize(option.label) || option.label,
|
||||
onClick: () => handleAddVariable(option.label, option.value),
|
||||
}))}
|
||||
menuId={menuId}
|
||||
className="z-30"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import MultiMessage from './MultiMessage';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
export default function MessagesView({
|
||||
messagesTree: _messagesTree,
|
||||
@@ -9,6 +10,7 @@ export default function MessagesView({
|
||||
messagesTree?: TMessage[] | null;
|
||||
conversationId: string;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const [currentEditId, setCurrentEditId] = useState<number | string | null>(-1);
|
||||
return (
|
||||
<div className="flex-1 pb-[50px]">
|
||||
@@ -23,7 +25,7 @@ export default function MessagesView({
|
||||
<div className="flex flex-col pb-9 text-sm dark:bg-transparent">
|
||||
{(_messagesTree && _messagesTree.length == 0) || _messagesTree === null ? (
|
||||
<div className="flex w-full items-center justify-center gap-1 bg-gray-50 p-3 text-sm text-gray-500 dark:border-gray-800/50 dark:bg-gray-800 dark:text-gray-300">
|
||||
Nothing found
|
||||
{localize('com_ui_nothing_found')}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -10,6 +10,7 @@ import Action from '~/components/SidePanel/Builder/Action';
|
||||
import { ToolSelectDialog } from '~/components/Tools';
|
||||
import { icons } from '~/hooks/Endpoint/Icons';
|
||||
import { processAgentOption } from '~/utils';
|
||||
import Instructions from './Instructions';
|
||||
import AgentAvatar from './AgentAvatar';
|
||||
import FileContext from './FileContext';
|
||||
import { useLocalize } from '~/hooks';
|
||||
@@ -228,39 +229,7 @@ export default function AgentConfig({
|
||||
/>
|
||||
</div>
|
||||
{/* Instructions */}
|
||||
<div className="mb-4">
|
||||
<label className={labelClass} htmlFor="instructions">
|
||||
{localize('com_ui_instructions')}
|
||||
</label>
|
||||
<Controller
|
||||
name="instructions"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<>
|
||||
<textarea
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
// maxLength={32768}
|
||||
className={cn(inputClass, 'min-h-[100px] resize-y')}
|
||||
id="instructions"
|
||||
placeholder={localize('com_agents_instructions_placeholder')}
|
||||
rows={3}
|
||||
aria-label="Agent instructions"
|
||||
aria-required="true"
|
||||
aria-invalid={error ? 'true' : 'false'}
|
||||
/>
|
||||
{error && (
|
||||
<span
|
||||
className="text-sm text-red-500 transition duration-300 ease-in-out"
|
||||
role="alert"
|
||||
>
|
||||
{localize('com_ui_field_required')}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Instructions />
|
||||
{/* Model and Provider */}
|
||||
<div className="mb-4">
|
||||
<label className={labelClass} htmlFor="provider">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useWatch, useForm, FormProvider } from 'react-hook-form';
|
||||
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
|
||||
import {
|
||||
Tools,
|
||||
Constants,
|
||||
SystemRoles,
|
||||
EModelEndpoint,
|
||||
isAssistantsEndpoint,
|
||||
@@ -45,7 +46,7 @@ export default function AgentPanel({
|
||||
|
||||
const modelsQuery = useGetModelsQuery();
|
||||
const agentQuery = useGetAgentByIdQuery(current_agent_id ?? '', {
|
||||
enabled: !!(current_agent_id ?? ''),
|
||||
enabled: !!(current_agent_id ?? '') && current_agent_id !== Constants.EPHEMERAL_AGENT_ID,
|
||||
});
|
||||
|
||||
const models = useMemo(() => modelsQuery.data ?? {}, [modelsQuery.data]);
|
||||
|
||||
127
client/src/components/SidePanel/Agents/Instructions.tsx
Normal file
127
client/src/components/SidePanel/Agents/Instructions.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import React, { useState, useId } from 'react';
|
||||
import { PlusCircle } from 'lucide-react';
|
||||
import * as Menu from '@ariakit/react/menu';
|
||||
import { specialVariables } from 'librechat-data-provider';
|
||||
import type { TSpecialVarLabel } from 'librechat-data-provider';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import type { AgentForm } from '~/common';
|
||||
import { cn, defaultTextProps, removeFocusOutlines } from '~/utils';
|
||||
// import ControlCombobox from '~/components/ui/ControlCombobox';
|
||||
import { DropdownPopup } from '~/components';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
const inputClass = cn(
|
||||
defaultTextProps,
|
||||
'flex w-full px-3 py-2 border-border-light bg-surface-secondary focus-visible:ring-2 focus-visible:ring-ring-primary',
|
||||
removeFocusOutlines,
|
||||
);
|
||||
|
||||
interface VariableOption {
|
||||
label: TSpecialVarLabel;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const variableOptions: VariableOption[] = Object.keys(specialVariables).map((key) => ({
|
||||
label: `com_ui_special_var_${key}` as TSpecialVarLabel,
|
||||
value: `{{${key}}}`,
|
||||
}));
|
||||
|
||||
export default function Instructions() {
|
||||
const menuId = useId();
|
||||
const localize = useLocalize();
|
||||
const methods = useFormContext<AgentForm>();
|
||||
const { control, setValue, getValues } = methods;
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
const handleAddVariable = (label: TSpecialVarLabel, value: string) => {
|
||||
const currentInstructions = getValues('instructions') || '';
|
||||
const spacer = currentInstructions.length > 0 ? '\n' : '';
|
||||
const prefix = localize(label);
|
||||
setValue('instructions', currentInstructions + spacer + prefix + ': ' + value);
|
||||
setIsMenuOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 flex items-center">
|
||||
<label className="text-token-text-primary flex-grow font-medium" htmlFor="instructions">
|
||||
{localize('com_ui_instructions')}
|
||||
</label>
|
||||
<div className="ml-auto" title="Add variables to instructions">
|
||||
{/* ControlCombobox implementation
|
||||
<ControlCombobox
|
||||
selectedValue=""
|
||||
displayValue="Add variables"
|
||||
items={variableOptions.map((option) => ({
|
||||
label: option.label,
|
||||
value: option.value,
|
||||
}))}
|
||||
setValue={handleAddVariable}
|
||||
ariaLabel="Add variable to instructions"
|
||||
searchPlaceholder="Search variables"
|
||||
selectPlaceholder="Add"
|
||||
isCollapsed={false}
|
||||
SelectIcon={<PlusCircle className="h-3 w-3 text-text-secondary" />}
|
||||
containerClassName="w-fit"
|
||||
className="h-7 gap-1 rounded-md border border-border-medium bg-surface-secondary px-2 py-0 text-sm text-text-primary transition-colors duration-200 hover:bg-surface-tertiary"
|
||||
iconSide="left"
|
||||
showCarat={false}
|
||||
/>
|
||||
*/}
|
||||
<DropdownPopup
|
||||
portal={true}
|
||||
mountByState={true}
|
||||
unmountOnHide={true}
|
||||
preserveTabOrder={true}
|
||||
isOpen={isMenuOpen}
|
||||
setIsOpen={setIsMenuOpen}
|
||||
trigger={
|
||||
<Menu.MenuButton
|
||||
id="variables-menu-button"
|
||||
aria-label="Add variable to instructions"
|
||||
className="flex h-7 items-center gap-1 rounded-md border border-border-medium bg-surface-secondary px-2 py-0 text-sm text-text-primary transition-colors duration-200 hover:bg-surface-tertiary"
|
||||
>
|
||||
<PlusCircle className="mr-1 h-3 w-3 text-text-secondary" aria-hidden={true} />
|
||||
{localize('com_ui_variables')}
|
||||
</Menu.MenuButton>
|
||||
}
|
||||
items={variableOptions.map((option) => ({
|
||||
label: localize(option.label) || option.label,
|
||||
onClick: () => handleAddVariable(option.label, option.value),
|
||||
}))}
|
||||
menuId={menuId}
|
||||
className="z-30"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Controller
|
||||
name="instructions"
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<>
|
||||
<textarea
|
||||
{...field}
|
||||
value={field.value ?? ''}
|
||||
className={cn(inputClass, 'min-h-[100px] resize-y')}
|
||||
id="instructions"
|
||||
placeholder={localize('com_agents_instructions_placeholder')}
|
||||
rows={3}
|
||||
aria-label="Agent instructions"
|
||||
aria-required="true"
|
||||
aria-invalid={error ? 'true' : 'false'}
|
||||
/>
|
||||
{error && (
|
||||
<span
|
||||
className="text-sm text-red-500 transition duration-300 ease-in-out"
|
||||
role="alert"
|
||||
>
|
||||
{localize('com_ui_field_required')}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ interface OGDialogProps extends DialogPrimitive.DialogProps {
|
||||
}
|
||||
|
||||
const Dialog = React.forwardRef<HTMLDivElement, OGDialogProps>(
|
||||
({ children, triggerRef, onOpenChange, ...props }) => {
|
||||
({ children, triggerRef, onOpenChange, ...props }, _ref) => {
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open && triggerRef?.current) {
|
||||
setTimeout(() => {
|
||||
@@ -71,6 +71,7 @@ const DialogContent = React.forwardRef<
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-ring-primary ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
{/* eslint-disable-next-line i18next/no-literal-string */}
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
|
||||
@@ -90,33 +90,37 @@ const SplitText: React.FC<SplitTextProps> = ({
|
||||
}, [inView, text, onLineCountChange]);
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
className={`split-parent inline overflow-hidden ${className}`}
|
||||
style={{ textAlign, whiteSpace: 'normal', wordWrap: 'break-word' }}
|
||||
>
|
||||
{words.map((word, wordIndex) => (
|
||||
<span key={wordIndex} style={{ display: 'inline-block', whiteSpace: 'nowrap' }}>
|
||||
{word.map((letter, letterIndex) => {
|
||||
const index =
|
||||
words.slice(0, wordIndex).reduce((acc, w) => acc + w.length, 0) + letterIndex;
|
||||
<>
|
||||
<span className="sr-only">{text}</span>
|
||||
<p
|
||||
ref={ref}
|
||||
className={`split-parent inline overflow-hidden ${className}`}
|
||||
style={{ textAlign, whiteSpace: 'normal', wordWrap: 'break-word' }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{words.map((word, wordIndex) => (
|
||||
<span key={wordIndex} style={{ display: 'inline-block', whiteSpace: 'nowrap' }}>
|
||||
{word.map((letter, letterIndex) => {
|
||||
const index =
|
||||
words.slice(0, wordIndex).reduce((acc, w) => acc + w.length, 0) + letterIndex;
|
||||
|
||||
return (
|
||||
<animated.span
|
||||
key={index}
|
||||
style={springs[index] as unknown as React.CSSProperties}
|
||||
className="inline-block transform transition-opacity will-change-transform"
|
||||
>
|
||||
{letter}
|
||||
</animated.span>
|
||||
);
|
||||
})}
|
||||
{wordIndex < words.length - 1 && (
|
||||
<span style={{ display: 'inline-block', width: '0.3em' }}> </span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
return (
|
||||
<animated.span
|
||||
key={index}
|
||||
style={springs[index] as unknown as React.CSSProperties}
|
||||
className="inline-block transform transition-opacity will-change-transform"
|
||||
>
|
||||
{letter}
|
||||
</animated.span>
|
||||
);
|
||||
})}
|
||||
{wordIndex < words.length - 1 && (
|
||||
<span style={{ display: 'inline-block', width: '0.3em' }}> </span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// export * from './queries';
|
||||
export * from './queries';
|
||||
export * from './mutations';
|
||||
|
||||
42
client/src/data-provider/Messages/queries.ts
Normal file
42
client/src/data-provider/Messages/queries.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import type { UseQueryOptions, QueryObserverResult } from '@tanstack/react-query';
|
||||
import { QueryKeys, dataService } from 'librechat-data-provider';
|
||||
import type * as t from 'librechat-data-provider';
|
||||
import { logger } from '~/utils';
|
||||
|
||||
export const useGetMessagesByConvoId = <TData = t.TMessage[]>(
|
||||
id: string,
|
||||
config?: UseQueryOptions<t.TMessage[], unknown, TData>,
|
||||
): QueryObserverResult<TData> => {
|
||||
const location = useLocation();
|
||||
const queryClient = useQueryClient();
|
||||
return useQuery<t.TMessage[], unknown, TData>(
|
||||
[QueryKeys.messages, id],
|
||||
async () => {
|
||||
const result = await dataService.getMessagesByConvoId(id);
|
||||
if (!location.pathname.includes('/c/new') && result?.length === 1) {
|
||||
const currentMessages = queryClient.getQueryData<t.TMessage[]>([QueryKeys.messages, id]);
|
||||
if (currentMessages?.length === 1) {
|
||||
return result;
|
||||
}
|
||||
if (currentMessages && currentMessages?.length > 1) {
|
||||
logger.warn(
|
||||
'messages',
|
||||
`Messages query for convo ${id} returned fewer than cache; path: "${location.pathname}"`,
|
||||
result,
|
||||
currentMessages,
|
||||
);
|
||||
return currentMessages;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
...config,
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -18,7 +18,7 @@ export default function useSelectAgent() {
|
||||
);
|
||||
|
||||
const agentQuery = useGetAgentByIdQuery(selectedAgentId ?? '', {
|
||||
enabled: !!(selectedAgentId ?? ''),
|
||||
enabled: !!(selectedAgentId ?? '') && selectedAgentId !== Constants.EPHEMERAL_AGENT_ID,
|
||||
});
|
||||
|
||||
const updateConversation = useCallback(
|
||||
|
||||
@@ -2,3 +2,5 @@ export { default as useChatHelpers } from './useChatHelpers';
|
||||
export { default as useAddedHelpers } from './useAddedHelpers';
|
||||
export { default as useAddedResponse } from './useAddedResponse';
|
||||
export { default as useChatFunctions } from './useChatFunctions';
|
||||
export { default as useIdChangeEffect } from './useIdChangeEffect';
|
||||
export { default as useFocusChatEffect } from './useFocusChatEffect';
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ContentTypes,
|
||||
EModelEndpoint,
|
||||
parseCompactConvo,
|
||||
replaceSpecialVars,
|
||||
isAssistantsEndpoint,
|
||||
} from 'librechat-data-provider';
|
||||
import { useSetRecoilState, useResetRecoilState, useRecoilValue } from 'recoil';
|
||||
@@ -26,6 +27,7 @@ import { getArtifactsMode } from '~/utils/artifacts';
|
||||
import { getEndpointField, logger } from '~/utils';
|
||||
import useUserKey from '~/hooks/Input/useUserKey';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuthContext } from '~/hooks';
|
||||
|
||||
const logChatRequest = (request: Record<string, unknown>) => {
|
||||
logger.log('=====================================\nAsk function called with:');
|
||||
@@ -66,19 +68,19 @@ export default function useChatFunctions({
|
||||
setSubmission: SetterOrUpdater<TSubmission | null>;
|
||||
setLatestMessage?: SetterOrUpdater<TMessage | null>;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const getSender = useGetSender();
|
||||
const { user } = useAuthContext();
|
||||
const queryClient = useQueryClient();
|
||||
const setFilesToDelete = useSetFilesToDelete();
|
||||
const getEphemeralAgent = useGetEphemeralAgent();
|
||||
const isTemporary = useRecoilValue(store.isTemporary);
|
||||
const codeArtifacts = useRecoilValue(store.codeArtifacts);
|
||||
const includeShadcnui = useRecoilValue(store.includeShadcnui);
|
||||
const customPromptMode = useRecoilValue(store.customPromptMode);
|
||||
const navigate = useNavigate();
|
||||
const resetLatestMultiMessage = useResetRecoilState(store.latestMessageFamily(index + 1));
|
||||
const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index));
|
||||
const setFilesToDelete = useSetFilesToDelete();
|
||||
const getSender = useGetSender();
|
||||
const isTemporary = useRecoilValue(store.isTemporary);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { getExpiry } = useUserKey(conversation?.endpoint ?? '');
|
||||
const customPromptMode = useRecoilValue(store.customPromptMode);
|
||||
const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index));
|
||||
const resetLatestMultiMessage = useResetRecoilState(store.latestMessageFamily(index + 1));
|
||||
|
||||
const ask: TAskFunction = (
|
||||
{
|
||||
@@ -128,6 +130,13 @@ export default function useChatFunctions({
|
||||
|
||||
let currentMessages: TMessage[] | null = overrideMessages ?? getMessages() ?? [];
|
||||
|
||||
if (conversation?.promptPrefix) {
|
||||
conversation.promptPrefix = replaceSpecialVars({
|
||||
text: conversation.promptPrefix,
|
||||
user,
|
||||
});
|
||||
}
|
||||
|
||||
// construct the query message
|
||||
// this is not a real messageId, it is used as placeholder before real messageId returned
|
||||
text = text.trim();
|
||||
@@ -148,7 +157,7 @@ export default function useChatFunctions({
|
||||
parentMessageId = Constants.NO_PARENT;
|
||||
currentMessages = [];
|
||||
conversationId = null;
|
||||
navigate('/c/new');
|
||||
navigate('/c/new', { state: { focusChat: true } });
|
||||
}
|
||||
|
||||
const targetParentMessageId = isRegenerate ? messageId : latestMessage?.parentMessageId;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { QueryKeys } from 'librechat-data-provider';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { useGetMessagesByConvoId } from 'librechat-data-provider/react-query';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import useChatFunctions from '~/hooks/Chat/useChatFunctions';
|
||||
import { useGetMessagesByConvoId } from '~/data-provider';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import useNewConvo from '~/hooks/useNewConvo';
|
||||
import store from '~/store';
|
||||
|
||||
18
client/src/hooks/Chat/useFocusChatEffect.ts
Normal file
18
client/src/hooks/Chat/useFocusChatEffect.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { logger } from '~/utils';
|
||||
|
||||
export default function useFocusChatEffect(textAreaRef: React.RefObject<HTMLTextAreaElement>) {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
if (textAreaRef?.current && location.state?.focusChat) {
|
||||
logger.log(
|
||||
'conversation',
|
||||
`Focusing textarea on location state change: ${location.pathname}`,
|
||||
);
|
||||
textAreaRef.current?.focus();
|
||||
navigate(`${location.pathname}${location.search ?? ''}`, { replace: true, state: {} });
|
||||
}
|
||||
}, [navigate, textAreaRef, location.pathname, location.state?.focusChat, location.search]);
|
||||
}
|
||||
21
client/src/hooks/Chat/useIdChangeEffect.ts
Normal file
21
client/src/hooks/Chat/useIdChangeEffect.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useResetRecoilState } from 'recoil';
|
||||
import { logger } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
/**
|
||||
* Hook to reset artifacts when the conversation ID changes
|
||||
* @param conversationId - The current conversation ID
|
||||
*/
|
||||
export default function useIdChangeEffect(conversationId: string) {
|
||||
const lastConvoId = useRef<string | null>(null);
|
||||
const resetArtifacts = useResetRecoilState(store.artifactsState);
|
||||
|
||||
useEffect(() => {
|
||||
if (conversationId !== lastConvoId.current) {
|
||||
logger.log('conversation', 'Conversation ID change');
|
||||
resetArtifacts();
|
||||
}
|
||||
lastConvoId.current = conversationId;
|
||||
}, [conversationId, resetArtifacts]);
|
||||
}
|
||||
@@ -1,13 +1,7 @@
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
QueryKeys,
|
||||
Constants,
|
||||
dataService,
|
||||
EModelEndpoint,
|
||||
LocalStorageKeys,
|
||||
} from 'librechat-data-provider';
|
||||
import { QueryKeys, Constants } from 'librechat-data-provider';
|
||||
import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider';
|
||||
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField, logger } from '~/utils';
|
||||
import store from '~/store';
|
||||
@@ -16,46 +10,28 @@ const useNavigateToConvo = (index = 0) => {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const clearAllConversations = store.useClearConvoState();
|
||||
const clearAllLatestMessages = store.useClearLatestMessages(`useNavigateToConvo ${index}`);
|
||||
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
|
||||
const clearAllLatestMessages = store.useClearLatestMessages(`useNavigateToConvo ${index}`);
|
||||
const { hasSetConversation, setConversation } = store.useCreateConversationAtom(index);
|
||||
|
||||
const fetchFreshData = async (conversationId?: string | null) => {
|
||||
if (!conversationId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await queryClient.fetchQuery([QueryKeys.conversation, conversationId], () =>
|
||||
dataService.getConversationById(conversationId),
|
||||
);
|
||||
logger.log('conversation', 'Fetched fresh conversation data', data);
|
||||
await queryClient.invalidateQueries([QueryKeys.messages, conversationId]);
|
||||
setConversation(data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching conversation data on navigation', error);
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToConvo = (
|
||||
conversation?: TConversation | null,
|
||||
_resetLatestMessage = true,
|
||||
/** Likely need to remove this since it happens after fetching conversation data */
|
||||
invalidateMessages = false,
|
||||
options?: {
|
||||
resetLatestMessage?: boolean;
|
||||
currentConvoId?: string;
|
||||
},
|
||||
) => {
|
||||
if (!conversation) {
|
||||
logger.warn('conversation', 'Conversation not provided to `navigateToConvo`');
|
||||
return;
|
||||
}
|
||||
const { resetLatestMessage = true, currentConvoId } = options || {};
|
||||
logger.log('conversation', 'Navigating to conversation', conversation);
|
||||
hasSetConversation.current = true;
|
||||
setSubmission(null);
|
||||
if (_resetLatestMessage) {
|
||||
if (resetLatestMessage) {
|
||||
clearAllLatestMessages();
|
||||
}
|
||||
if (invalidateMessages && conversation.conversationId != null && conversation.conversationId) {
|
||||
queryClient.setQueryData([QueryKeys.messages, Constants.NEW_CONVO], []);
|
||||
queryClient.invalidateQueries([QueryKeys.messages, conversation.conversationId]);
|
||||
}
|
||||
|
||||
let convo = { ...conversation };
|
||||
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
|
||||
@@ -83,48 +59,12 @@ const useNavigateToConvo = (index = 0) => {
|
||||
}
|
||||
clearAllConversations(true);
|
||||
setConversation(convo);
|
||||
navigate(`/c/${convo.conversationId ?? Constants.NEW_CONVO}`);
|
||||
if (convo.conversationId !== Constants.NEW_CONVO && convo.conversationId) {
|
||||
queryClient.invalidateQueries([QueryKeys.conversation, convo.conversationId]);
|
||||
fetchFreshData(convo.conversationId);
|
||||
}
|
||||
};
|
||||
|
||||
const navigateWithLastTools = (
|
||||
conversation?: TConversation | null,
|
||||
_resetLatestMessage?: boolean,
|
||||
invalidateMessages?: boolean,
|
||||
) => {
|
||||
if (!conversation) {
|
||||
logger.warn('conversation', 'Conversation not provided to `navigateToConvo`');
|
||||
return;
|
||||
}
|
||||
// set conversation to the new conversation
|
||||
if (conversation.endpoint === EModelEndpoint.gptPlugins) {
|
||||
let lastSelectedTools = [];
|
||||
try {
|
||||
lastSelectedTools =
|
||||
JSON.parse(localStorage.getItem(LocalStorageKeys.LAST_TOOLS) ?? '') ?? [];
|
||||
} catch (e) {
|
||||
logger.error('conversation', 'Error parsing last selected tools', e);
|
||||
}
|
||||
const hasTools = (conversation.tools?.length ?? 0) > 0;
|
||||
navigateToConvo(
|
||||
{
|
||||
...conversation,
|
||||
tools: hasTools ? conversation.tools : lastSelectedTools,
|
||||
},
|
||||
_resetLatestMessage,
|
||||
invalidateMessages,
|
||||
);
|
||||
} else {
|
||||
navigateToConvo(conversation, _resetLatestMessage, invalidateMessages);
|
||||
}
|
||||
queryClient.setQueryData([QueryKeys.messages, currentConvoId], []);
|
||||
navigate(`/c/${convo.conversationId ?? Constants.NEW_CONVO}`, { state: { focusChat: true } });
|
||||
};
|
||||
|
||||
return {
|
||||
navigateToConvo,
|
||||
navigateWithLastTools,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -96,13 +96,14 @@ export default function useTextarea({
|
||||
return localize('com_endpoint_message_not_appendable');
|
||||
}
|
||||
|
||||
const sender = isAssistant || isAgent
|
||||
? getEntityName({ name: entityName, isAgent, localize })
|
||||
: getSender(conversation as TEndpointOption);
|
||||
const sender =
|
||||
isAssistant || isAgent
|
||||
? getEntityName({ name: entityName, isAgent, localize })
|
||||
: getSender(conversation as TEndpointOption);
|
||||
|
||||
return `${localize(
|
||||
'com_endpoint_message_new', { 0: sender ? sender : localize('com_endpoint_ai') },
|
||||
)}`;
|
||||
return `${localize('com_endpoint_message_new', {
|
||||
0: sender ? sender : localize('com_endpoint_ai'),
|
||||
})}`;
|
||||
};
|
||||
|
||||
const placeholder = getPlaceholderText();
|
||||
@@ -237,7 +238,8 @@ export default function useTextarea({
|
||||
textAreaRef,
|
||||
handlePaste,
|
||||
handleKeyDown,
|
||||
handleCompositionStart,
|
||||
isNotAppendable,
|
||||
handleCompositionEnd,
|
||||
handleCompositionStart,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { v4 } from 'uuid';
|
||||
import { useCallback } from 'react';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { Constants, replaceSpecialVars } from 'librechat-data-provider';
|
||||
import { useChatContext, useChatFormContext, useAddedChatContext } from '~/Providers';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import { replaceSpecialVars } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
const appendIndex = (index: number, value?: string) => {
|
||||
|
||||
@@ -20,12 +20,13 @@ import type { TGenTitleMutation } from '~/data-provider';
|
||||
import type { SetterOrUpdater, Resetter } from 'recoil';
|
||||
import type { ConversationCursorData } from '~/utils';
|
||||
import {
|
||||
logger,
|
||||
scrollToEnd,
|
||||
getAllContentText,
|
||||
addConvoToAllQueries,
|
||||
updateConvoInAllQueries,
|
||||
removeConvoFromAllQueries,
|
||||
findConversationInInfinite,
|
||||
getAllContentText,
|
||||
} from '~/utils';
|
||||
import useAttachmentHandler from '~/hooks/SSE/useAttachmentHandler';
|
||||
import useContentHandler from '~/hooks/SSE/useContentHandler';
|
||||
@@ -487,10 +488,6 @@ export default function useEventHandlers({
|
||||
}
|
||||
|
||||
if (setConversation && isAddedRequest !== true) {
|
||||
if (location.pathname === '/c/new') {
|
||||
navigate(`/c/${conversation.conversationId}`, { replace: true });
|
||||
}
|
||||
|
||||
setConversation((prevState) => {
|
||||
const update = {
|
||||
...prevState,
|
||||
@@ -508,6 +505,9 @@ export default function useEventHandlers({
|
||||
}
|
||||
return update;
|
||||
});
|
||||
if (location.pathname === '/c/new') {
|
||||
navigate(`/c/${conversation.conversationId}`, { replace: true });
|
||||
}
|
||||
}
|
||||
|
||||
setIsSubmitting(false);
|
||||
@@ -536,6 +536,12 @@ export default function useEventHandlers({
|
||||
const conversationId =
|
||||
userMessage.conversationId ?? submission.conversation?.conversationId ?? '';
|
||||
|
||||
const setErrorMessages = (convoId: string, errorMessage: TMessage) => {
|
||||
const finalMessages: TMessage[] = [...messages, userMessage, errorMessage];
|
||||
setMessages(finalMessages);
|
||||
queryClient.setQueryData<TMessage[]>([QueryKeys.messages, convoId], finalMessages);
|
||||
};
|
||||
|
||||
const parseErrorResponse = (data: TResData | Partial<TMessage>) => {
|
||||
const metadata = data['responseMessage'] ?? data;
|
||||
const errorMessage: Partial<TMessage> = {
|
||||
@@ -553,7 +559,7 @@ export default function useEventHandlers({
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
const convoId = conversationId || v4();
|
||||
const convoId = conversationId || `_${v4()}`;
|
||||
const errorMetadata = parseErrorResponse({
|
||||
text: 'Error connecting to server, try refreshing the page.',
|
||||
...submission,
|
||||
@@ -564,7 +570,7 @@ export default function useEventHandlers({
|
||||
getMessages,
|
||||
submission,
|
||||
});
|
||||
setMessages([...messages, userMessage, errorResponse]);
|
||||
setErrorMessages(convoId, errorResponse);
|
||||
if (newConversation) {
|
||||
newConversation({
|
||||
template: { conversationId: convoId },
|
||||
@@ -577,9 +583,9 @@ export default function useEventHandlers({
|
||||
|
||||
const receivedConvoId = data.conversationId ?? '';
|
||||
if (!conversationId && !receivedConvoId) {
|
||||
const convoId = v4();
|
||||
const convoId = `_${v4()}`;
|
||||
const errorResponse = parseErrorResponse(data);
|
||||
setMessages([...messages, userMessage, errorResponse]);
|
||||
setErrorMessages(convoId, errorResponse);
|
||||
if (newConversation) {
|
||||
newConversation({
|
||||
template: { conversationId: convoId },
|
||||
@@ -590,7 +596,7 @@ export default function useEventHandlers({
|
||||
return;
|
||||
} else if (!receivedConvoId) {
|
||||
const errorResponse = parseErrorResponse(data);
|
||||
setMessages([...messages, userMessage, errorResponse]);
|
||||
setErrorMessages(conversationId, errorResponse);
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
@@ -601,7 +607,7 @@ export default function useEventHandlers({
|
||||
parentMessageId: userMessage.messageId,
|
||||
});
|
||||
|
||||
setMessages([...messages, userMessage, errorResponse]);
|
||||
setErrorMessages(receivedConvoId, errorResponse);
|
||||
if (receivedConvoId && paramId === Constants.NEW_CONVO && newConversation) {
|
||||
newConversation({
|
||||
template: { conversationId: receivedConvoId },
|
||||
@@ -612,7 +618,15 @@ export default function useEventHandlers({
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
},
|
||||
[setCompleted, setMessages, paramId, newConversation, setIsSubmitting, getMessages],
|
||||
[
|
||||
setCompleted,
|
||||
setMessages,
|
||||
paramId,
|
||||
newConversation,
|
||||
setIsSubmitting,
|
||||
getMessages,
|
||||
queryClient,
|
||||
],
|
||||
);
|
||||
|
||||
const abortConversation = useCallback(
|
||||
@@ -649,9 +663,11 @@ export default function useEventHandlers({
|
||||
);
|
||||
return;
|
||||
} else if (!isAssistantsEndpoint(endpoint)) {
|
||||
const convoId = conversationId || `_${v4()}`;
|
||||
logger.log('conversation', 'Aborted conversation with minimal messages, ID: ' + convoId);
|
||||
if (newConversation) {
|
||||
newConversation({
|
||||
template: { conversationId: conversationId || v4() },
|
||||
template: { conversationId: convoId },
|
||||
preset: tPresetSchema.parse(submission.conversation),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,10 +58,14 @@ export const ThemeProvider = ({ initialTheme, children }) => {
|
||||
if (fontSize == null) {
|
||||
setFontSize('text-base');
|
||||
applyFontSize('text-base');
|
||||
localStorage.setItem('fontSize', 'text-base');
|
||||
localStorage.setItem('fontSize', JSON.stringify('text-base'));
|
||||
return;
|
||||
}
|
||||
applyFontSize(JSON.parse(fontSize));
|
||||
try {
|
||||
applyFontSize(JSON.parse(fontSize));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
// Reason: This effect should only run once, and `setFontSize` is a stable function
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Constants,
|
||||
FileSources,
|
||||
@@ -30,12 +30,12 @@ import { useDeleteFilesMutation, useGetEndpointsQuery, useGetStartupConfig } fro
|
||||
import useAssistantListMap from './Assistants/useAssistantListMap';
|
||||
import { useResetChatBadges } from './useChatBadges';
|
||||
import { usePauseGlobalAudio } from './Audio';
|
||||
import { mainTextareaId } from '~/common';
|
||||
import { logger } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
const useNewConvo = (index = 0) => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const clearAllConversations = store.useClearConvoState();
|
||||
const defaultPreset = useRecoilValue(store.defaultPreset);
|
||||
@@ -47,7 +47,6 @@ const useNewConvo = (index = 0) => {
|
||||
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
|
||||
|
||||
const modelsQuery = useGetModelsQuery();
|
||||
const timeoutIdRef = useRef<NodeJS.Timeout>();
|
||||
const assistantsListMap = useAssistantListMap();
|
||||
const { pauseGlobalAudio } = usePauseGlobalAudio(index);
|
||||
const saveDrafts = useRecoilValue<boolean>(store.saveDrafts);
|
||||
@@ -152,31 +151,47 @@ const useNewConvo = (index = 0) => {
|
||||
if (!(keepAddedConvos ?? false)) {
|
||||
clearAllConversations(true);
|
||||
}
|
||||
logger.log('conversation', 'Setting conversation from `useNewConvo`', conversation);
|
||||
setConversation(conversation);
|
||||
const isCancelled = conversation.conversationId?.startsWith('_');
|
||||
if (isCancelled) {
|
||||
logger.log(
|
||||
'conversation',
|
||||
'Cancelled conversation, setting to `new` in `useNewConvo`',
|
||||
conversation,
|
||||
);
|
||||
setConversation({
|
||||
...conversation,
|
||||
conversationId: 'new',
|
||||
});
|
||||
} else {
|
||||
logger.log('conversation', 'Setting conversation from `useNewConvo`', conversation);
|
||||
setConversation(conversation);
|
||||
}
|
||||
setSubmission({} as TSubmission);
|
||||
if (!(keepLatestMessage ?? false)) {
|
||||
clearAllLatestMessages();
|
||||
}
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const searchParamsString = searchParams?.toString();
|
||||
const getParams = () => (searchParamsString ? `?${searchParamsString}` : '');
|
||||
|
||||
if (conversation.conversationId === Constants.NEW_CONVO && !modelsData) {
|
||||
const appTitle = localStorage.getItem(LocalStorageKeys.APP_TITLE) ?? '';
|
||||
if (appTitle) {
|
||||
document.title = appTitle;
|
||||
}
|
||||
navigate(`/c/${Constants.NEW_CONVO}`);
|
||||
}
|
||||
|
||||
clearTimeout(timeoutIdRef.current);
|
||||
if (disableFocus === true) {
|
||||
const path = `/c/${Constants.NEW_CONVO}${getParams()}`;
|
||||
navigate(path, { state: { focusChat: true } });
|
||||
return;
|
||||
}
|
||||
timeoutIdRef.current = setTimeout(() => {
|
||||
const textarea = document.getElementById(mainTextareaId);
|
||||
if (textarea) {
|
||||
textarea.focus();
|
||||
}
|
||||
}, 150);
|
||||
|
||||
const path = `/c/${conversation.conversationId}${getParams()}`;
|
||||
navigate(path, {
|
||||
replace: true,
|
||||
state: disableFocus ? {} : { focusChat: true },
|
||||
});
|
||||
},
|
||||
[endpointsConfig, defaultPreset, assistantsListMap, modelsQuery.data],
|
||||
);
|
||||
|
||||
@@ -665,7 +665,6 @@
|
||||
"com_ui_simple": "بسيط",
|
||||
"com_ui_size": "الحجم",
|
||||
"com_ui_special_variables": "المتغيرات الخاصة:",
|
||||
"com_ui_special_variables_info": "استخدم `{{current_date}}` للتاريخ الحالي، و `{{current_user}}` لاسم حسابك المحدد.",
|
||||
"com_ui_speech_while_submitting": "لا يمكن إرسال الكلام أثناء إنشاء الرد",
|
||||
"com_ui_stop": "توقف",
|
||||
"com_ui_storage": "التخزين",
|
||||
|
||||
@@ -678,7 +678,6 @@
|
||||
"com_ui_simple": "Jednoduché",
|
||||
"com_ui_size": "Velikost",
|
||||
"com_ui_special_variables": "Speciální proměnné:",
|
||||
"com_ui_special_variables_info": "Použijte `{{current_date}}` pro aktuální datum a `{{current_user}}` pro vaše uživatelské jméno.",
|
||||
"com_ui_speech_while_submitting": "Nelze odeslat hlasový vstup, zatímco se generuje odpověď",
|
||||
"com_ui_stop": "Zastavit",
|
||||
"com_ui_storage": "Úložiště",
|
||||
|
||||
@@ -801,7 +801,6 @@
|
||||
"com_ui_simple": "Einfach",
|
||||
"com_ui_size": "Größe",
|
||||
"com_ui_special_variables": "Spezielle Variablen:",
|
||||
"com_ui_special_variables_info": "Verwende `{{current_date}}` für das aktuelle Datum und `{{current_user}}` für deinen angegebenen Kontonamen.",
|
||||
"com_ui_speech_while_submitting": "Spracheingabe nicht möglich während eine Antwort generiert wird",
|
||||
"com_ui_stop": "Stopp",
|
||||
"com_ui_storage": "Speicher",
|
||||
|
||||
@@ -542,6 +542,7 @@
|
||||
"com_ui_bulk_delete_error": "Failed to delete shared links",
|
||||
"com_ui_callback_url": "Callback URL",
|
||||
"com_ui_cancel": "Cancel",
|
||||
"com_ui_category": "Category",
|
||||
"com_ui_chat": "Chat",
|
||||
"com_ui_chat_history": "Chat History",
|
||||
"com_ui_clear": "Clear",
|
||||
@@ -651,10 +652,15 @@
|
||||
"com_ui_fork_info_2": "\"Forking\" refers to creating a new conversation that start/end from specific messages in the current conversation, creating a copy according to the options selected.",
|
||||
"com_ui_fork_info_3": "The \"target message\" refers to either the message this popup was opened from, or, if you check \"{{0}}\", the latest message in the conversation.",
|
||||
"com_ui_fork_info_branches": "This option forks the visible messages, along with related branches; in other words, the direct path to the target message, including branches along the path.",
|
||||
"com_ui_fork_info_button_label": "View information about forking conversations",
|
||||
"com_ui_fork_info_remember": "Check this to remember the options you select for future usage, making it quicker to fork conversations as preferred.",
|
||||
"com_ui_fork_info_start": "If checked, forking will commence from this message to the latest message in the conversation, according to the behavior selected above.",
|
||||
"com_ui_fork_info_target": "This option forks all messages leading up to the target message, including its neighbors; in other words, all message branches, whether or not they are visible or along the same path, are included.",
|
||||
"com_ui_fork_info_visible": "This option forks only the visible messages; in other words, the direct path to the target message, without any branches.",
|
||||
"com_ui_fork_more_details_about": "View additional information and details about the \"{{0}}\" fork option",
|
||||
"com_ui_fork_more_info_options": "View detailed explanation of all fork options and their behaviors",
|
||||
"com_ui_fork_more_info_remember": "View explanation of how the \"{{0}}\" option saves your preferences for future forks",
|
||||
"com_ui_fork_more_info_split_target": "View explanation of how the \"{{0}}\" option affects which messages are included in your fork",
|
||||
"com_ui_fork_processing": "Forking conversation...",
|
||||
"com_ui_fork_remember": "Remember",
|
||||
"com_ui_fork_remember_checked": "Your selection will be remembered after usage. Change this at any time in the settings.",
|
||||
@@ -806,9 +812,14 @@
|
||||
"com_ui_sign_in_to_domain": "Sign-in to {{0}}",
|
||||
"com_ui_simple": "Simple",
|
||||
"com_ui_size": "Size",
|
||||
"com_ui_special_var_current_date": "Current Date",
|
||||
"com_ui_special_var_current_datetime": "Current Date & Time",
|
||||
"com_ui_special_var_current_user": "Current User",
|
||||
"com_ui_special_var_iso_datetime": "UTC ISO Datetime",
|
||||
"com_ui_special_variables": "Special variables:",
|
||||
"com_ui_special_variables_info": "Use `{{current_date}}` for the current date, and `{{current_user}}` for your given account name.",
|
||||
"com_ui_special_variables_more_info": "You can select special variables from the dropdown: `{{current_date}}` (today's date and day of week), `{{current_datetime}}` (local date and time), `{{utc_iso_datetime}}` (UTC ISO datetime), and `{{current_user}}` (your account name).",
|
||||
"com_ui_speech_while_submitting": "Can't submit speech while a response is being generated",
|
||||
"com_ui_sr_actions_menu": "Open actions menu for \"{{0}}\"",
|
||||
"com_ui_stop": "Stop",
|
||||
"com_ui_storage": "Storage",
|
||||
"com_ui_submit": "Submit",
|
||||
|
||||
@@ -677,7 +677,6 @@
|
||||
"com_ui_simple": "Simple",
|
||||
"com_ui_size": "Tamaño",
|
||||
"com_ui_special_variables": "Variables especiales:",
|
||||
"com_ui_special_variables_info": "Utilice `{{current_date}}` para la fecha actual y `{{current_user}}` para su nombre de cuenta asignado.",
|
||||
"com_ui_speech_while_submitting": "No se puede enviar un mensaje de voz mientras se está generando una respuesta",
|
||||
"com_ui_stop": "Detener",
|
||||
"com_ui_storage": "Almacenamiento",
|
||||
|
||||
@@ -801,7 +801,6 @@
|
||||
"com_ui_simple": "Lihtne",
|
||||
"com_ui_size": "Suurus",
|
||||
"com_ui_special_variables": "Erilised muutujad:",
|
||||
"com_ui_special_variables_info": "Kasuta `{{praegune_kuupäev}}` praeguse kuupäeva jaoks ja `{{praegune_kasutaja}}` oma konto nime jaoks.",
|
||||
"com_ui_speech_while_submitting": "Kõnet ei saa esitada, kui vastust genereeritakse",
|
||||
"com_ui_stop": "Peata",
|
||||
"com_ui_storage": "Salvestusruum",
|
||||
|
||||
@@ -801,7 +801,6 @@
|
||||
"com_ui_simple": "ساده",
|
||||
"com_ui_size": "اندازه",
|
||||
"com_ui_special_variables": "متغیرهای ویژه:",
|
||||
"com_ui_special_variables_info": "استفاده از `{{current_date}}` برای تاریخ فعلی، و`{{current_user}}برای نام حساب داده شده شما.",
|
||||
"com_ui_speech_while_submitting": "وقتی پاسخی در حال تولید است، نمیتوان گفتار ارسال کرد",
|
||||
"com_ui_stop": "توقف کنید",
|
||||
"com_ui_storage": "ذخیره سازی",
|
||||
|
||||
@@ -682,7 +682,6 @@
|
||||
"com_ui_simple": "Simple",
|
||||
"com_ui_size": "Taille",
|
||||
"com_ui_special_variables": "Variables spéciales : Utilisez {{current_date}} pour la date actuelle, et {{current_user}} pour le nom de votre compte donné.",
|
||||
"com_ui_special_variables_info": "Utilisez `{{current_date}}` pour la date actuelle et `{{current_user}}` pour votre nom de compte.",
|
||||
"com_ui_speech_while_submitting": "Impossible de soumettre un message vocal pendant la génération d'une réponse",
|
||||
"com_ui_stop": "Arrêt ",
|
||||
"com_ui_storage": "Stockage",
|
||||
|
||||
@@ -797,7 +797,6 @@
|
||||
"com_ui_simple": "פשוט",
|
||||
"com_ui_size": "סוג",
|
||||
"com_ui_special_variables": "משתנים מיוחדים:",
|
||||
"com_ui_special_variables_info": "השתמש ב-`{{current_date}}` עבור התאריך הנוכחי, וב-`{{current_user}}` עבור שם החשבון שלך.",
|
||||
"com_ui_speech_while_submitting": "לא ניתן לשלוח אודיו בזמן שנוצרת תגובה",
|
||||
"com_ui_stop": "עצור",
|
||||
"com_ui_storage": "אחסון",
|
||||
|
||||
@@ -801,7 +801,6 @@
|
||||
"com_ui_simple": "Egyszerű",
|
||||
"com_ui_size": "Méret",
|
||||
"com_ui_special_variables": "Speciális változók:",
|
||||
"com_ui_special_variables_info": "Használja a `{{current_date}}` változókat az aktuális dátumhoz, és a `{{current_user}}` változókat a megadott fióknevéhez.",
|
||||
"com_ui_speech_while_submitting": "Nem lehet beszédet beküldeni, amíg válasz generálódik",
|
||||
"com_ui_stop": "Leállítás",
|
||||
"com_ui_storage": "Tárolás",
|
||||
|
||||
@@ -783,7 +783,6 @@
|
||||
"com_ui_simple": "Semplice",
|
||||
"com_ui_size": "Dimensione",
|
||||
"com_ui_special_variables": "Variabili speciali:",
|
||||
"com_ui_special_variables_info": "Usa `{{current_date}}` per la data attuale e `{{current_user}}` per il nome del tuo account.",
|
||||
"com_ui_speech_while_submitting": "Impossibile inviare il messaggio mentre è in corso la generazione di una risposta",
|
||||
"com_ui_stop": "Ferma",
|
||||
"com_ui_storage": "Archiviazione",
|
||||
|
||||
@@ -665,7 +665,6 @@
|
||||
"com_ui_simple": "シンプル",
|
||||
"com_ui_size": "サイズ",
|
||||
"com_ui_special_variables": "特殊変数:",
|
||||
"com_ui_special_variables_info": "`{{current_date}}`は現在の日付、`{{current_user}}`は指定されたアカウント名に使用します。",
|
||||
"com_ui_speech_while_submitting": "応答の生成中は音声を送信できません",
|
||||
"com_ui_stop": "止める",
|
||||
"com_ui_storage": "ストレージ",
|
||||
|
||||
@@ -674,7 +674,6 @@
|
||||
"com_ui_simple": "간단",
|
||||
"com_ui_size": "크기",
|
||||
"com_ui_special_variables": "특수 변수:",
|
||||
"com_ui_special_variables_info": "현재 날짜는 `{{current_date}}`, 계정 이름은 `{{current_user}}`로 표시됩니다.",
|
||||
"com_ui_speech_while_submitting": "응답 생성 중에는 음성을 전송할 수 없습니다",
|
||||
"com_ui_stop": "중지",
|
||||
"com_ui_storage": "저장소",
|
||||
|
||||
@@ -669,7 +669,6 @@
|
||||
"com_ui_simple": "Prosty",
|
||||
"com_ui_size": "Rozmiar",
|
||||
"com_ui_special_variables": "Zmienne specjalne:",
|
||||
"com_ui_special_variables_info": "Użyj `{{current_date}}` dla aktualnej daty i `{{current_user}}` dla swojej nazwy konta.",
|
||||
"com_ui_speech_while_submitting": "Nie można przesłać mowy podczas generowania odpowiedzi",
|
||||
"com_ui_storage": "Przechowywanie",
|
||||
"com_ui_submit": "Wyślij",
|
||||
|
||||
@@ -773,7 +773,6 @@
|
||||
"com_ui_simple": "Simples",
|
||||
"com_ui_size": "Tamanho",
|
||||
"com_ui_special_variables": "Variáveis especiais:",
|
||||
"com_ui_special_variables_info": "Use `{{current_date}}` para a data atual, e `{{current_user}}` para o nome da sua conta.",
|
||||
"com_ui_speech_while_submitting": "Não é possível enviar a fala enquanto uma resposta está sendo gerada",
|
||||
"com_ui_stop": "Parar",
|
||||
"com_ui_storage": "Armazenamento",
|
||||
|
||||
@@ -773,7 +773,6 @@
|
||||
"com_ui_simple": "Simples",
|
||||
"com_ui_size": "Tamanho",
|
||||
"com_ui_special_variables": "Variáveis especiais:",
|
||||
"com_ui_special_variables_info": "Use `{{current_date}}` para a data atual, e `{{current_user}}` para o nome da sua conta.",
|
||||
"com_ui_speech_while_submitting": "Não é possível submeter fala enquanto a resposta está a ser gerada.",
|
||||
"com_ui_stop": "Parar",
|
||||
"com_ui_storage": "Armazenamento",
|
||||
|
||||
@@ -788,7 +788,6 @@
|
||||
"com_ui_simple": "Простой",
|
||||
"com_ui_size": "Размер",
|
||||
"com_ui_special_variables": "Специальные переменные:",
|
||||
"com_ui_special_variables_info": "Используйте `{{current_date}}` для отображения текущей даты и `{{current_user}}` для отображения имени вашей учетной записи.",
|
||||
"com_ui_speech_while_submitting": "Невозможно отправить голосовой ввод во время генерации ответа",
|
||||
"com_ui_stop": "Остановить генерацию",
|
||||
"com_ui_storage": "Хранилище",
|
||||
|
||||
@@ -760,7 +760,6 @@
|
||||
"com_ui_simple": "เรียบง่าย",
|
||||
"com_ui_size": "ขนาด",
|
||||
"com_ui_special_variables": "ตัวแปรพิเศษ:",
|
||||
"com_ui_special_variables_info": "ใช้ {{current_date}} สำหรับวันที่ปัจจุบัน และ {{current_user}} สำหรับชื่อบัญชีของคุณ",
|
||||
"com_ui_speech_while_submitting": "ไม่สามารถส่งเสียงพูดขณะกำลังสร้างคำตอบ",
|
||||
"com_ui_stop": "หยุด",
|
||||
"com_ui_storage": "ที่เก็บข้อมูล",
|
||||
|
||||
@@ -692,7 +692,6 @@
|
||||
"com_ui_simple": "Basit",
|
||||
"com_ui_size": "Boyut",
|
||||
"com_ui_special_variables": "Özel değişkenler:",
|
||||
"com_ui_special_variables_info": "Geçerli tarih için `{{current_date}}` ve hesap adınız için `{{current_user}}` kullanın.",
|
||||
"com_ui_speech_while_submitting": "Bir yanıt oluşturulurken konuşma gönderilemez",
|
||||
"com_ui_stop": "Durdur",
|
||||
"com_ui_storage": "Depolama",
|
||||
|
||||
@@ -798,7 +798,6 @@
|
||||
"com_ui_simple": "基本",
|
||||
"com_ui_size": "大小",
|
||||
"com_ui_special_variables": "特殊变量:",
|
||||
"com_ui_special_variables_info": "使用 `{{current_date}}` 获取当前日期,使用 `{{current_user}}` 获取您的账户名称。",
|
||||
"com_ui_speech_while_submitting": "正在生成回复时无法提交语音",
|
||||
"com_ui_stop": "停止",
|
||||
"com_ui_storage": "存储",
|
||||
|
||||
@@ -665,7 +665,6 @@
|
||||
"com_ui_simple": "簡單",
|
||||
"com_ui_size": "大小",
|
||||
"com_ui_special_variables": "特殊變數:",
|
||||
"com_ui_special_variables_info": "使用 `{{current_date}}` 可顯示目前日期,使用 `{{current_user}}` 可顯示您的帳戶名稱。",
|
||||
"com_ui_speech_while_submitting": "正在產生回應時無法送出語音",
|
||||
"com_ui_stop": "停止",
|
||||
"com_ui_storage": "儲存空間",
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
useGetStartupConfig,
|
||||
useGetEndpointsQuery,
|
||||
} from '~/data-provider';
|
||||
import { useNewConvo, useAppStartup, useAssistantListMap, useIdChangeEffect } from '~/hooks';
|
||||
import { getDefaultModelSpec, getModelSpecPreset, logger } from '~/utils';
|
||||
import { useNewConvo, useAppStartup, useAssistantListMap } from '~/hooks';
|
||||
import { ToolCallsMapProvider } from '~/Providers';
|
||||
import ChatView from '~/components/Chat/ChatView';
|
||||
import useAuthRedirect from './useAuthRedirect';
|
||||
@@ -34,7 +34,7 @@ export default function ChatRoute() {
|
||||
|
||||
const index = 0;
|
||||
const { conversationId = '' } = useParams();
|
||||
|
||||
useIdChangeEffect(conversationId);
|
||||
const { hasSetConversation, conversation } = store.useCreateConversationAtom(index);
|
||||
const { newConversation } = useNewConvo();
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ const getEndpointFromSetup = (
|
||||
if (targetEndpoint && endpointsConfig?.[targetEndpoint]) {
|
||||
return targetEndpoint as EModelEndpoint;
|
||||
} else if (targetEndpoint) {
|
||||
console.warn(`Illegal target endpoint ${targetEndpoint} ${endpointsConfig}`);
|
||||
console.warn(`Illegal target endpoint ${targetEndpoint}`, endpointsConfig);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
import { format } from 'date-fns';
|
||||
import type { TUser, TPromptGroup } from 'librechat-data-provider';
|
||||
|
||||
export function replaceSpecialVars({ text, user }: { text: string; user?: TUser }) {
|
||||
if (!text) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const currentDate = format(new Date(), 'yyyy-MM-dd');
|
||||
text = text.replace(/{{current_date}}/gi, currentDate);
|
||||
|
||||
if (!user) {
|
||||
return text;
|
||||
}
|
||||
const currentUser = user.name;
|
||||
text = text.replace(/{{current_user}}/gi, currentUser);
|
||||
|
||||
return text;
|
||||
}
|
||||
import { specialVariables } from 'librechat-data-provider';
|
||||
import type { TPromptGroup } from 'librechat-data-provider';
|
||||
|
||||
/**
|
||||
* Detects the presence of variables in the given text, excluding {{current_date}} and {{current_user}}.
|
||||
* Detects the presence of variables in the given text, excluding those found in `specialVariables`.
|
||||
*/
|
||||
export const detectVariables = (text: string): boolean => {
|
||||
const regex = /{{(?!current_date|current_user)[^{}]{1,}}}/gi;
|
||||
return regex.test(text);
|
||||
// Extract all variables with a simple regex
|
||||
const allVariablesRegex = /{{([^{}]+?)}}/gi;
|
||||
const matches = Array.from(text.matchAll(allVariablesRegex)).map((match) =>
|
||||
match[1].trim().toLowerCase(),
|
||||
);
|
||||
|
||||
// Check if any non-special variables exist
|
||||
return matches.some((variable) => !specialVariables[variable]);
|
||||
};
|
||||
|
||||
export const wrapVariable = (variable: string) => `{{${variable}}}`;
|
||||
@@ -94,11 +83,14 @@ export function formatDateTime(dateTimeString: string) {
|
||||
}
|
||||
|
||||
export const mapPromptGroups = (groups: TPromptGroup[]): Record<string, TPromptGroup> => {
|
||||
return groups.reduce((acc, group) => {
|
||||
if (!group._id) {
|
||||
return groups.reduce(
|
||||
(acc, group) => {
|
||||
if (!group._id) {
|
||||
return acc;
|
||||
}
|
||||
acc[group._id] = group;
|
||||
return acc;
|
||||
}
|
||||
acc[group._id] = group;
|
||||
return acc;
|
||||
}, {} as Record<string, TPromptGroup>);
|
||||
},
|
||||
{} as Record<string, TPromptGroup>,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// v0.7.7
|
||||
// v0.7.8-rc1
|
||||
// See .env.test.example for an example of the '.env.test' file.
|
||||
require('dotenv').config({ path: './e2e/.env.test' });
|
||||
|
||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "LibreChat",
|
||||
"version": "v0.7.7",
|
||||
"version": "v0.7.8-rc1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "LibreChat",
|
||||
"version": "v0.7.7",
|
||||
"version": "v0.7.8-rc1",
|
||||
"license": "ISC",
|
||||
"workspaces": [
|
||||
"api",
|
||||
@@ -47,7 +47,7 @@
|
||||
},
|
||||
"api": {
|
||||
"name": "@librechat/backend",
|
||||
"version": "v0.7.7",
|
||||
"version": "v0.7.8-rc1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.37.0",
|
||||
@@ -1161,7 +1161,7 @@
|
||||
},
|
||||
"client": {
|
||||
"name": "@librechat/frontend",
|
||||
"version": "v0.7.7",
|
||||
"version": "v0.7.8-rc1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@ariakit/react": "^0.4.15",
|
||||
@@ -26585,6 +26585,12 @@
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
@@ -43594,10 +43600,11 @@
|
||||
},
|
||||
"packages/data-provider": {
|
||||
"name": "librechat-data-provider",
|
||||
"version": "0.7.792",
|
||||
"version": "0.7.81",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"js-yaml": "^4.1.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "LibreChat",
|
||||
"version": "v0.7.7",
|
||||
"version": "v0.7.8-rc1",
|
||||
"description": "",
|
||||
"workspaces": [
|
||||
"api",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "librechat-data-provider",
|
||||
"version": "0.7.792",
|
||||
"version": "0.7.81",
|
||||
"description": "data services for librechat apps",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.es.js",
|
||||
@@ -40,6 +40,7 @@
|
||||
"homepage": "https://librechat.ai",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"js-yaml": "^4.1.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
|
||||
125
packages/data-provider/specs/parsers.spec.ts
Normal file
125
packages/data-provider/specs/parsers.spec.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { replaceSpecialVars } from '../src/parsers';
|
||||
import { specialVariables } from '../src/config';
|
||||
import type { TUser } from '../src/types';
|
||||
|
||||
// Mock dayjs module with consistent date/time values regardless of environment
|
||||
jest.mock('dayjs', () => {
|
||||
// Create a mock implementation that returns fixed values
|
||||
const mockDayjs = () => ({
|
||||
format: (format: string) => {
|
||||
if (format === 'YYYY-MM-DD') {
|
||||
return '2024-04-29';
|
||||
}
|
||||
if (format === 'YYYY-MM-DD HH:mm:ss') {
|
||||
return '2024-04-29 12:34:56';
|
||||
}
|
||||
return format; // fallback
|
||||
},
|
||||
day: () => 1, // 1 = Monday
|
||||
toISOString: () => '2024-04-29T16:34:56.000Z',
|
||||
});
|
||||
|
||||
// Add any static methods needed
|
||||
mockDayjs.extend = jest.fn();
|
||||
|
||||
return mockDayjs;
|
||||
});
|
||||
|
||||
describe('replaceSpecialVars', () => {
|
||||
// Create a partial user object for testing
|
||||
const mockUser = {
|
||||
name: 'Test User',
|
||||
id: 'user123',
|
||||
} as TUser;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should return the original text if text is empty', () => {
|
||||
expect(replaceSpecialVars({ text: '' })).toBe('');
|
||||
expect(replaceSpecialVars({ text: null as unknown as string })).toBe(null);
|
||||
expect(replaceSpecialVars({ text: undefined as unknown as string })).toBe(undefined);
|
||||
});
|
||||
|
||||
test('should replace {{current_date}} with the current date', () => {
|
||||
const result = replaceSpecialVars({ text: 'Today is {{current_date}}' });
|
||||
// dayjs().day() returns 1 for Monday (April 29, 2024 is a Monday)
|
||||
expect(result).toBe('Today is 2024-04-29 (1)');
|
||||
});
|
||||
|
||||
test('should replace {{current_datetime}} with the current datetime', () => {
|
||||
const result = replaceSpecialVars({ text: 'Now is {{current_datetime}}' });
|
||||
expect(result).toBe('Now is 2024-04-29 12:34:56 (1)');
|
||||
});
|
||||
|
||||
test('should replace {{iso_datetime}} with the ISO datetime', () => {
|
||||
const result = replaceSpecialVars({ text: 'ISO time: {{iso_datetime}}' });
|
||||
expect(result).toBe('ISO time: 2024-04-29T16:34:56.000Z');
|
||||
});
|
||||
|
||||
test('should replace {{current_user}} with the user name if provided', () => {
|
||||
const result = replaceSpecialVars({
|
||||
text: 'Hello {{current_user}}!',
|
||||
user: mockUser,
|
||||
});
|
||||
expect(result).toBe('Hello Test User!');
|
||||
});
|
||||
|
||||
test('should not replace {{current_user}} if user is not provided', () => {
|
||||
const result = replaceSpecialVars({
|
||||
text: 'Hello {{current_user}}!',
|
||||
});
|
||||
expect(result).toBe('Hello {{current_user}}!');
|
||||
});
|
||||
|
||||
test('should not replace {{current_user}} if user has no name', () => {
|
||||
const result = replaceSpecialVars({
|
||||
text: 'Hello {{current_user}}!',
|
||||
user: { id: 'user123' } as TUser,
|
||||
});
|
||||
expect(result).toBe('Hello {{current_user}}!');
|
||||
});
|
||||
|
||||
test('should handle multiple replacements in the same text', () => {
|
||||
const result = replaceSpecialVars({
|
||||
text: 'Hello {{current_user}}! Today is {{current_date}} and the time is {{current_datetime}}. ISO: {{iso_datetime}}',
|
||||
user: mockUser,
|
||||
});
|
||||
expect(result).toBe(
|
||||
'Hello Test User! Today is 2024-04-29 (1) and the time is 2024-04-29 12:34:56 (1). ISO: 2024-04-29T16:34:56.000Z',
|
||||
);
|
||||
});
|
||||
|
||||
test('should be case-insensitive when replacing variables', () => {
|
||||
const result = replaceSpecialVars({
|
||||
text: 'Date: {{CURRENT_DATE}}, User: {{Current_User}}',
|
||||
user: mockUser,
|
||||
});
|
||||
expect(result).toBe('Date: 2024-04-29 (1), User: Test User');
|
||||
});
|
||||
|
||||
test('should confirm all specialVariables from config.ts get parsed', () => {
|
||||
// Create a text that includes all special variables
|
||||
const specialVarsText = Object.keys(specialVariables)
|
||||
.map((key) => `{{${key}}}`)
|
||||
.join(' ');
|
||||
|
||||
const result = replaceSpecialVars({
|
||||
text: specialVarsText,
|
||||
user: mockUser,
|
||||
});
|
||||
|
||||
// Verify none of the original variable placeholders remain in the result
|
||||
Object.keys(specialVariables).forEach((key) => {
|
||||
const placeholder = `{{${key}}}`;
|
||||
expect(result).not.toContain(placeholder);
|
||||
});
|
||||
|
||||
// Verify the expected replacements
|
||||
expect(result).toContain('2024-04-29 (1)'); // current_date
|
||||
expect(result).toContain('2024-04-29 12:34:56 (1)'); // current_datetime
|
||||
expect(result).toContain('2024-04-29T16:34:56.000Z'); // iso_datetime
|
||||
expect(result).toContain('Test User'); // current_user
|
||||
});
|
||||
});
|
||||
@@ -1227,7 +1227,7 @@ export enum TTSProviders {
|
||||
/** Enum for app-wide constants */
|
||||
export enum Constants {
|
||||
/** Key for the app's version. */
|
||||
VERSION = 'v0.7.7',
|
||||
VERSION = 'v0.7.8-rc1',
|
||||
/** Key for the Custom Config's version (librechat.yaml). */
|
||||
CONFIG_VERSION = '1.2.4',
|
||||
/** Standard value for the first message's `parentMessageId` value, to indicate no parent exists. */
|
||||
@@ -1349,3 +1349,12 @@ export const providerEndpointMap = {
|
||||
[EModelEndpoint.anthropic]: EModelEndpoint.anthropic,
|
||||
[EModelEndpoint.azureOpenAI]: EModelEndpoint.azureOpenAI,
|
||||
};
|
||||
|
||||
export const specialVariables = {
|
||||
current_date: true,
|
||||
current_user: true,
|
||||
iso_datetime: true,
|
||||
current_datetime: true,
|
||||
};
|
||||
|
||||
export type TSpecialVarLabel = `com_ui_special_var_${keyof typeof specialVariables}`;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import dayjs from 'dayjs';
|
||||
import type { ZodIssue } from 'zod';
|
||||
import type * as a from './types/assistants';
|
||||
import type * as s from './schemas';
|
||||
@@ -252,6 +253,8 @@ export const getResponseSender = (endpointOption: t.TEndpointOption): string =>
|
||||
return extractOmniVersion(model);
|
||||
} else if (model && (model.includes('mistral') || model.includes('codestral'))) {
|
||||
return 'Mistral';
|
||||
} else if (model && model.includes('deepseek')) {
|
||||
return 'Deepseek';
|
||||
} else if (model && model.includes('gpt-')) {
|
||||
const gptVersion = extractGPTVersion(model);
|
||||
return gptVersion || 'GPT';
|
||||
@@ -288,6 +291,8 @@ export const getResponseSender = (endpointOption: t.TEndpointOption): string =>
|
||||
return extractOmniVersion(model);
|
||||
} else if (model && (model.includes('mistral') || model.includes('codestral'))) {
|
||||
return 'Mistral';
|
||||
} else if (model && model.includes('deepseek')) {
|
||||
return 'Deepseek';
|
||||
} else if (model && model.includes('gpt-')) {
|
||||
const gptVersion = extractGPTVersion(model);
|
||||
return gptVersion || 'GPT';
|
||||
@@ -418,3 +423,28 @@ export function findLastSeparatorIndex(text: string, separators = SEPARATORS): n
|
||||
}
|
||||
return lastIndex;
|
||||
}
|
||||
|
||||
export function replaceSpecialVars({ text, user }: { text: string; user?: t.TUser | null }) {
|
||||
let result = text;
|
||||
if (!result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// e.g., "2024-04-29 (1)" (1=Monday)
|
||||
const currentDate = dayjs().format('YYYY-MM-DD');
|
||||
const dayNumber = dayjs().day();
|
||||
const combinedDate = `${currentDate} (${dayNumber})`;
|
||||
result = result.replace(/{{current_date}}/gi, combinedDate);
|
||||
|
||||
const currentDatetime = dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||
result = result.replace(/{{current_datetime}}/gi, `${currentDatetime} (${dayNumber})`);
|
||||
|
||||
const isoDatetime = dayjs().toISOString();
|
||||
result = result.replace(/{{iso_datetime}}/gi, isoDatetime);
|
||||
|
||||
if (user && user.name) {
|
||||
result = result.replace(/{{current_user}}/gi, user.name);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -29,22 +29,6 @@ export const useAbortRequestWithMessage = (): UseMutationResult<
|
||||
);
|
||||
};
|
||||
|
||||
export const useGetMessagesByConvoId = <TData = s.TMessage[]>(
|
||||
id: string,
|
||||
config?: UseQueryOptions<s.TMessage[], unknown, TData>,
|
||||
): QueryObserverResult<TData> => {
|
||||
return useQuery<s.TMessage[], unknown, TData>(
|
||||
[QueryKeys.messages, id],
|
||||
() => dataService.getMessagesByConvoId(id),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
...config,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const useGetSharedMessages = (
|
||||
shareId: string,
|
||||
config?: UseQueryOptions<t.TSharedMessagesResponse>,
|
||||
|
||||
@@ -29,7 +29,9 @@ function dropSchemaFields(
|
||||
schema: JsonSchemaType | undefined,
|
||||
fields: string[],
|
||||
): JsonSchemaType | undefined {
|
||||
if (schema == null || typeof schema !== 'object') {return schema;}
|
||||
if (schema == null || typeof schema !== 'object') {
|
||||
return schema;
|
||||
}
|
||||
// Handle arrays (should only occur for enum, required, etc.)
|
||||
if (Array.isArray(schema)) {
|
||||
// This should not happen for the root schema, but for completeness:
|
||||
@@ -37,33 +39,25 @@ function dropSchemaFields(
|
||||
}
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(schema)) {
|
||||
if (fields.includes(key)) {continue;}
|
||||
if (fields.includes(key)) {
|
||||
continue;
|
||||
}
|
||||
// Recursively process nested schemas
|
||||
if (
|
||||
key === 'items' ||
|
||||
key === 'additionalProperties' ||
|
||||
key === 'properties'
|
||||
) {
|
||||
if (key === 'items' || key === 'additionalProperties' || key === 'properties') {
|
||||
if (key === 'properties' && value && typeof value === 'object') {
|
||||
// properties is a record of string -> JsonSchemaType
|
||||
const newProps: Record<string, JsonSchemaType> = {};
|
||||
for (const [propKey, propValue] of Object.entries(
|
||||
value as Record<string, JsonSchemaType>,
|
||||
)) {
|
||||
const dropped = dropSchemaFields(
|
||||
propValue,
|
||||
fields,
|
||||
);
|
||||
const dropped = dropSchemaFields(propValue, fields);
|
||||
if (dropped !== undefined) {
|
||||
newProps[propKey] = dropped;
|
||||
}
|
||||
}
|
||||
result[key] = newProps;
|
||||
} else if (key === 'items' || key === 'additionalProperties') {
|
||||
const dropped = dropSchemaFields(
|
||||
value as JsonSchemaType,
|
||||
fields,
|
||||
);
|
||||
const dropped = dropSchemaFields(value as JsonSchemaType, fields);
|
||||
if (dropped !== undefined) {
|
||||
result[key] = dropped;
|
||||
}
|
||||
@@ -127,12 +121,12 @@ function convertToZodUnion(
|
||||
|
||||
// Special handling for schemas that add properties
|
||||
if (subSchema.properties && Object.keys(subSchema.properties).length > 0) {
|
||||
// Create a schema with the properties and make them all optional
|
||||
// Create a schema with the properties and make them all optional
|
||||
const objSchema = {
|
||||
type: 'object',
|
||||
properties: subSchema.properties,
|
||||
additionalProperties: true, // Allow additional properties
|
||||
// Don't include required here to make all properties optional
|
||||
// Don't include required here to make all properties optional
|
||||
} as JsonSchemaType;
|
||||
|
||||
// Convert to Zod schema
|
||||
@@ -141,15 +135,17 @@ function convertToZodUnion(
|
||||
// For the special case of { optional: true }
|
||||
if ('optional' in (subSchema.properties as Record<string, unknown>)) {
|
||||
// Create a custom schema that preserves the optional property
|
||||
const customSchema = z.object({
|
||||
optional: z.boolean(),
|
||||
}).passthrough();
|
||||
const customSchema = z
|
||||
.object({
|
||||
optional: z.boolean(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
return customSchema;
|
||||
}
|
||||
|
||||
if (zodSchema instanceof z.ZodObject) {
|
||||
// Make sure the schema allows additional properties
|
||||
// Make sure the schema allows additional properties
|
||||
return zodSchema.passthrough();
|
||||
}
|
||||
return zodSchema;
|
||||
@@ -362,12 +358,15 @@ export function convertJsonSchemaToZod(
|
||||
const partial = Object.fromEntries(
|
||||
Object.entries(shape).map(([key, value]) => [
|
||||
key,
|
||||
schema.required?.includes(key) === true ? value : value.optional(),
|
||||
schema.required?.includes(key) === true ? value : value.optional().nullable(),
|
||||
]),
|
||||
);
|
||||
objectSchema = z.object(partial);
|
||||
} else {
|
||||
objectSchema = objectSchema.partial();
|
||||
const partialNullable = Object.fromEntries(
|
||||
Object.entries(shape).map(([key, value]) => [key, value.optional().nullable()]),
|
||||
);
|
||||
objectSchema = z.object(partialNullable);
|
||||
}
|
||||
|
||||
// Handle additionalProperties for open-ended objects
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user