Repository: coaidev/coai
Branch: main
Commit: 3048a493eedc
Files: 535
Total size: 2.2 MB
Directory structure:
gitextract__4v8y3mp/
├── .dockerignore
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ ├── channel_update.md
│ │ ├── config.yml
│ │ └── feature_request.md
│ └── workflows/
│ ├── app.yaml
│ ├── build.yaml
│ ├── docker-cd.yaml
│ ├── docker-ci.yaml
│ └── issue-translator.yaml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── README_ja-JP.md
├── README_zh-CN.md
├── adapter/
│ ├── adapter.go
│ ├── azure/
│ │ ├── chat.go
│ │ ├── image.go
│ │ ├── processor.go
│ │ ├── struct.go
│ │ └── types.go
│ ├── baichuan/
│ │ ├── chat.go
│ │ ├── processor.go
│ │ ├── struct.go
│ │ └── types.go
│ ├── bing/
│ │ ├── chat.go
│ │ ├── struct.go
│ │ └── types.go
│ ├── claude/
│ │ ├── chat.go
│ │ ├── struct.go
│ │ └── types.go
│ ├── common/
│ │ ├── interface.go
│ │ └── types.go
│ ├── coze/
│ │ ├── chat.go
│ │ ├── processor.go
│ │ └── struct.go
│ ├── dashscope/
│ │ ├── chat.go
│ │ ├── struct.go
│ │ └── types.go
│ ├── deepseek/
│ │ ├── chat.go
│ │ └── struct.go
│ ├── dify/
│ │ ├── chat.go
│ │ ├── processor.go
│ │ └── struct.go
│ ├── hunyuan/
│ │ ├── chat.go
│ │ ├── sdk.go
│ │ └── struct.go
│ ├── midjourney/
│ │ ├── api.go
│ │ ├── chat.go
│ │ ├── expose.go
│ │ ├── handler.go
│ │ ├── storage.go
│ │ ├── struct.go
│ │ └── types.go
│ ├── openai/
│ │ ├── chat.go
│ │ ├── image.go
│ │ ├── processor.go
│ │ ├── struct.go
│ │ ├── types.go
│ │ └── videos.go
│ ├── palm2/
│ │ ├── chat.go
│ │ ├── formatter.go
│ │ ├── image.go
│ │ ├── struct.go
│ │ └── types.go
│ ├── request.go
│ ├── router.go
│ ├── skylark/
│ │ ├── chat.go
│ │ ├── formatter.go
│ │ └── struct.go
│ ├── slack/
│ │ ├── chat.go
│ │ └── struct.go
│ ├── sparkdesk/
│ │ ├── chat.go
│ │ ├── struct.go
│ │ └── types.go
│ ├── zhinao/
│ │ ├── chat.go
│ │ ├── processor.go
│ │ ├── struct.go
│ │ └── types.go
│ └── zhipuai/
│ ├── chat.go
│ ├── processor.go
│ ├── struct.go
│ └── types.go
├── addition/
│ ├── article/
│ │ ├── api.go
│ │ ├── data/
│ │ │ └── .gitkeep
│ │ ├── generate.go
│ │ ├── template.docx
│ │ └── utils.go
│ ├── card/
│ │ ├── .gitignore
│ │ ├── card.go
│ │ ├── card.php
│ │ ├── error.php
│ │ └── utils.php
│ ├── generation/
│ │ ├── api.go
│ │ ├── build.go
│ │ ├── data/
│ │ │ └── .gitkeep
│ │ ├── generate.go
│ │ └── prompt.go
│ ├── router.go
│ └── web/
│ ├── call.go
│ └── search.go
├── admin/
│ ├── analysis/
│ │ ├── analysis.go
│ │ ├── format.go
│ │ ├── reflect.go
│ │ ├── statistic.go
│ │ └── types.go
│ ├── controller.go
│ ├── format.go
│ ├── instance.go
│ ├── invitation.go
│ ├── logger.go
│ ├── market.go
│ ├── redeem.go
│ ├── router.go
│ ├── statistic.go
│ ├── types.go
│ └── user.go
├── app/
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── .prettierrc.json
│ ├── components.json
│ ├── index.html
│ ├── package.json
│ ├── postcss.config.js
│ ├── public/
│ │ ├── manifest.json
│ │ ├── robots.txt
│ │ ├── service.js
│ │ ├── site.webmanifest
│ │ └── workbox.js
│ ├── qodana.yaml
│ ├── src/
│ │ ├── App.tsx
│ │ ├── admin/
│ │ │ ├── api/
│ │ │ │ ├── channel.ts
│ │ │ │ ├── charge.ts
│ │ │ │ ├── chart.ts
│ │ │ │ ├── info.ts
│ │ │ │ ├── logger.ts
│ │ │ │ ├── market.ts
│ │ │ │ ├── plan.ts
│ │ │ │ └── system.ts
│ │ │ ├── channel.ts
│ │ │ ├── charge.ts
│ │ │ ├── colors.ts
│ │ │ ├── datasets/
│ │ │ │ └── charge.ts
│ │ │ ├── hook.tsx
│ │ │ ├── market.ts
│ │ │ └── types.ts
│ │ ├── api/
│ │ │ ├── addition.ts
│ │ │ ├── auth.ts
│ │ │ ├── broadcast.ts
│ │ │ ├── common.ts
│ │ │ ├── connection.ts
│ │ │ ├── file.ts
│ │ │ ├── generation.ts
│ │ │ ├── history.ts
│ │ │ ├── mask.ts
│ │ │ ├── plugin.ts
│ │ │ ├── quota.ts
│ │ │ ├── record.ts
│ │ │ ├── redeem.ts
│ │ │ ├── sharing.ts
│ │ │ ├── types.tsx
│ │ │ └── v1.ts
│ │ ├── assets/
│ │ │ ├── admin/
│ │ │ │ ├── all.less
│ │ │ │ ├── broadcast.less
│ │ │ │ ├── channel.less
│ │ │ │ ├── charge.less
│ │ │ │ ├── dashboard.less
│ │ │ │ ├── logger.less
│ │ │ │ ├── management.less
│ │ │ │ ├── market.less
│ │ │ │ ├── menu.less
│ │ │ │ ├── subscription.less
│ │ │ │ └── system.less
│ │ │ ├── common/
│ │ │ │ ├── 404.less
│ │ │ │ ├── editor.less
│ │ │ │ ├── file.less
│ │ │ │ ├── loader.less
│ │ │ │ └── plugin.less
│ │ │ ├── fonts/
│ │ │ │ ├── all.less
│ │ │ │ ├── common.less
│ │ │ │ └── katex.less
│ │ │ ├── globals.less
│ │ │ ├── main.less
│ │ │ ├── markdown/
│ │ │ │ ├── all.less
│ │ │ │ ├── highlight.less
│ │ │ │ ├── style.less
│ │ │ │ └── theme.less
│ │ │ ├── pages/
│ │ │ │ ├── api.less
│ │ │ │ ├── article.less
│ │ │ │ ├── auth.less
│ │ │ │ ├── chat.less
│ │ │ │ ├── generation.less
│ │ │ │ ├── home.less
│ │ │ │ ├── navbar.less
│ │ │ │ ├── notify.less
│ │ │ │ ├── package.less
│ │ │ │ ├── preset.less
│ │ │ │ ├── quota.less
│ │ │ │ ├── record.less
│ │ │ │ ├── settings.less
│ │ │ │ ├── share-manager.less
│ │ │ │ ├── sharing.less
│ │ │ │ └── subscription.less
│ │ │ └── ui.less
│ │ ├── components/
│ │ │ ├── Avatar.tsx
│ │ │ ├── EditorProvider.tsx
│ │ │ ├── Emoji.tsx
│ │ │ ├── ErrorBoundary.tsx
│ │ │ ├── FileProvider.tsx
│ │ │ ├── FileViewer.tsx
│ │ │ ├── I18nProvider.tsx
│ │ │ ├── Loader.tsx
│ │ │ ├── MCPResultDebug.tsx
│ │ │ ├── MCPResultPanel.tsx
│ │ │ ├── Markdown.tsx
│ │ │ ├── Message.tsx
│ │ │ ├── ModelAvatar.tsx
│ │ │ ├── OperationAction.tsx
│ │ │ ├── Paragraph.tsx
│ │ │ ├── PopupDialog.tsx
│ │ │ ├── ProjectLink.tsx
│ │ │ ├── ReloadService.tsx
│ │ │ ├── Require.tsx
│ │ │ ├── SelectGroup.tsx
│ │ │ ├── ThemeProvider.tsx
│ │ │ ├── ThinkContent.tsx
│ │ │ ├── TickButton.tsx
│ │ │ ├── Tips.tsx
│ │ │ ├── TrendBadge.tsx
│ │ │ ├── VoiceProvider.tsx
│ │ │ ├── WarningButton.tsx
│ │ │ ├── admin/
│ │ │ │ ├── ChannelSettings.tsx
│ │ │ │ ├── ChargeWidget.tsx
│ │ │ │ ├── ChartBox.tsx
│ │ │ │ ├── InfoBox.tsx
│ │ │ │ ├── InvitationTable.tsx
│ │ │ │ ├── MenuBar.tsx
│ │ │ │ ├── RedeemTable.tsx
│ │ │ │ ├── UserTable.tsx
│ │ │ │ ├── assemblies/
│ │ │ │ │ ├── BillingChart.tsx
│ │ │ │ │ ├── BroadcastTable.tsx
│ │ │ │ │ ├── ChannelEditor.tsx
│ │ │ │ │ ├── ChannelTable.tsx
│ │ │ │ │ ├── ErrorChart.tsx
│ │ │ │ │ ├── ModelChart.tsx
│ │ │ │ │ ├── ModelUsageChart.tsx
│ │ │ │ │ ├── RequestChart.tsx
│ │ │ │ │ └── UserTypeChart.tsx
│ │ │ │ └── common/
│ │ │ │ └── StateBadge.tsx
│ │ │ ├── app/
│ │ │ │ ├── Announcement.tsx
│ │ │ │ ├── AppProvider.tsx
│ │ │ │ ├── Contact.tsx
│ │ │ │ ├── MenuBar.tsx
│ │ │ │ └── NavBar.tsx
│ │ │ ├── home/
│ │ │ │ ├── ChatInterface.tsx
│ │ │ │ ├── ChatSpace.tsx
│ │ │ │ ├── ChatWrapper.tsx
│ │ │ │ ├── ConversationItem.tsx
│ │ │ │ ├── MaskEditor.tsx
│ │ │ │ ├── ModelArea.tsx
│ │ │ │ ├── SideBar.tsx
│ │ │ │ ├── assemblies/
│ │ │ │ │ ├── ActionButton.tsx
│ │ │ │ │ ├── ChatAction.tsx
│ │ │ │ │ ├── ChatInput.tsx
│ │ │ │ │ └── ScrollAction.tsx
│ │ │ │ └── subscription/
│ │ │ │ ├── SubscriptionUsage.tsx
│ │ │ │ └── UpgradePlan.tsx
│ │ │ ├── markdown/
│ │ │ │ ├── Code.tsx
│ │ │ │ ├── Image.tsx
│ │ │ │ ├── Label.tsx
│ │ │ │ ├── Link.tsx
│ │ │ │ ├── Reference.tsx
│ │ │ │ ├── Video.tsx
│ │ │ │ └── VirtualMessage.tsx
│ │ │ ├── plugins/
│ │ │ │ ├── file.tsx
│ │ │ │ ├── mermaid.tsx
│ │ │ │ └── progress.tsx
│ │ │ ├── ui/
│ │ │ │ ├── accordion.tsx
│ │ │ │ ├── alert-dialog.tsx
│ │ │ │ ├── alert.tsx
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── calendar.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── checkbox.tsx
│ │ │ │ ├── clickable.tsx
│ │ │ │ ├── combo-box.tsx
│ │ │ │ ├── command.tsx
│ │ │ │ ├── context-menu.tsx
│ │ │ │ ├── date-picker.tsx
│ │ │ │ ├── dialog.tsx
│ │ │ │ ├── drawer.tsx
│ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ ├── icons/
│ │ │ │ │ ├── Github.tsx
│ │ │ │ │ └── Send.tsx
│ │ │ │ ├── input.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── lib/
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── multi-combobox.tsx
│ │ │ │ ├── number-input.tsx
│ │ │ │ ├── pagination.tsx
│ │ │ │ ├── popover.tsx
│ │ │ │ ├── progress.tsx
│ │ │ │ ├── radio-box.tsx
│ │ │ │ ├── radio-group.tsx
│ │ │ │ ├── scroll-area.tsx
│ │ │ │ ├── select.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── sheet.tsx
│ │ │ │ ├── skeleton.tsx
│ │ │ │ ├── slider.tsx
│ │ │ │ ├── sonner.tsx
│ │ │ │ ├── step.tsx
│ │ │ │ ├── switch.tsx
│ │ │ │ ├── table.tsx
│ │ │ │ ├── tabs.tsx
│ │ │ │ ├── textarea.tsx
│ │ │ │ ├── toast.tsx
│ │ │ │ ├── toaster.tsx
│ │ │ │ ├── toggle-group.tsx
│ │ │ │ ├── toggle.tsx
│ │ │ │ ├── tooltip.tsx
│ │ │ │ └── use-toast.ts
│ │ │ └── utils/
│ │ │ └── Icon.tsx
│ │ ├── conf/
│ │ │ ├── api.ts
│ │ │ ├── bootstrap.ts
│ │ │ ├── deeptrain.tsx
│ │ │ ├── env.ts
│ │ │ ├── model.ts
│ │ │ ├── storage.ts
│ │ │ ├── subscription.tsx
│ │ │ └── version.json
│ │ ├── dialogs/
│ │ │ ├── SettingsDialog.tsx
│ │ │ └── index.tsx
│ │ ├── events/
│ │ │ ├── blob.ts
│ │ │ ├── info.ts
│ │ │ ├── model.ts
│ │ │ ├── spinner.ts
│ │ │ ├── struct.ts
│ │ │ └── theme.ts
│ │ ├── i18n.ts
│ │ ├── main.tsx
│ │ ├── masks/
│ │ │ ├── prompts.ts
│ │ │ └── types.ts
│ │ ├── payment/
│ │ │ ├── icons.tsx
│ │ │ ├── request.ts
│ │ │ └── utils.ts
│ │ ├── plugin/
│ │ │ └── types.ts
│ │ ├── resources/
│ │ │ └── i18n/
│ │ │ ├── cn.json
│ │ │ ├── en.json
│ │ │ ├── ja.json
│ │ │ ├── ru.json
│ │ │ └── tw.json
│ │ ├── router.tsx
│ │ ├── routes/
│ │ │ ├── Account.tsx
│ │ │ ├── Admin.tsx
│ │ │ ├── Article.tsx
│ │ │ ├── Auth.tsx
│ │ │ ├── Forgot.tsx
│ │ │ ├── Generation.tsx
│ │ │ ├── Home.tsx
│ │ │ ├── Index.tsx
│ │ │ ├── Model.tsx
│ │ │ ├── NotFound.tsx
│ │ │ ├── Register.tsx
│ │ │ ├── Sharing.tsx
│ │ │ ├── Wallet.tsx
│ │ │ ├── admin/
│ │ │ │ ├── Broadcast.tsx
│ │ │ │ ├── Channel.tsx
│ │ │ │ ├── Charge.tsx
│ │ │ │ ├── DashBoard.tsx
│ │ │ │ ├── License.tsx
│ │ │ │ ├── Logger.tsx
│ │ │ │ ├── Market.tsx
│ │ │ │ ├── Notification.tsx
│ │ │ │ ├── Subscription.tsx
│ │ │ │ ├── System.tsx
│ │ │ │ ├── Users.tsx
│ │ │ │ └── common/
│ │ │ │ └── CommonAdminPage.tsx
│ │ │ └── wallet/
│ │ │ ├── AmountItem.tsx
│ │ │ └── WalletQuotaBox.tsx
│ │ ├── spinner.tsx
│ │ ├── store/
│ │ │ ├── api.ts
│ │ │ ├── auth.ts
│ │ │ ├── avatar.ts
│ │ │ ├── chat.ts
│ │ │ ├── globals.ts
│ │ │ ├── index.ts
│ │ │ ├── info.ts
│ │ │ ├── menu.ts
│ │ │ ├── package.ts
│ │ │ ├── quota.ts
│ │ │ ├── record.ts
│ │ │ ├── settings.ts
│ │ │ ├── sharing.ts
│ │ │ ├── subscription.ts
│ │ │ └── utils.ts
│ │ ├── translator/
│ │ │ ├── adapter.ts
│ │ │ ├── index.ts
│ │ │ ├── io.ts
│ │ │ └── translator.ts
│ │ ├── types/
│ │ │ ├── performance.d.ts
│ │ │ ├── service.d.ts
│ │ │ └── ui.d.ts
│ │ ├── utils/
│ │ │ ├── analytics.ts
│ │ │ ├── app.ts
│ │ │ ├── base.ts
│ │ │ ├── date.ts
│ │ │ ├── desktop.ts
│ │ │ ├── dev.ts
│ │ │ ├── device.ts
│ │ │ ├── dom.ts
│ │ │ ├── form.ts
│ │ │ ├── groups.ts
│ │ │ ├── hook.ts
│ │ │ ├── loader.tsx
│ │ │ ├── memory.ts
│ │ │ ├── path.ts
│ │ │ └── processor.ts
│ │ └── vite-env.d.ts
│ ├── src-tauri/
│ │ ├── .gitignore
│ │ ├── Cargo.toml
│ │ ├── build.rs
│ │ ├── icons/
│ │ │ └── icon.icns
│ │ ├── src/
│ │ │ └── main.rs
│ │ └── tauri.conf.json
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
├── auth/
│ ├── analysis.go
│ ├── apikey.go
│ ├── auth.go
│ ├── call.go
│ ├── cert.go
│ ├── controller.go
│ ├── invitation.go
│ ├── package.go
│ ├── payment.go
│ ├── quota.go
│ ├── redeem.go
│ ├── router.go
│ ├── rule.go
│ ├── struct.go
│ ├── subscription.go
│ ├── usage.go
│ └── validators.go
├── channel/
│ ├── channel.go
│ ├── charge.go
│ ├── controller.go
│ ├── manager.go
│ ├── plan.go
│ ├── router.go
│ ├── sequence.go
│ ├── system.go
│ ├── ticker.go
│ ├── types.go
│ └── worker.go
├── cli/
│ ├── admin.go
│ ├── exec.go
│ ├── help.go
│ ├── invite.go
│ ├── parser.go
│ └── token.go
├── config.example.yaml
├── connection/
│ ├── cache.go
│ ├── database.go
│ ├── db_migration.go
│ └── worker.go
├── docker-compose.stable.yaml
├── docker-compose.watch.yaml
├── docker-compose.yaml
├── globals/
│ ├── constant.go
│ ├── interface.go
│ ├── logger.go
│ ├── method.go
│ ├── params.go
│ ├── sql.go
│ ├── tools.go
│ ├── types.go
│ ├── usage.go
│ └── variables.go
├── go.mod
├── go.sum
├── main.go
├── manager/
│ ├── broadcast/
│ │ ├── controller.go
│ │ ├── manage.go
│ │ ├── router.go
│ │ ├── types.go
│ │ └── view.go
│ ├── chat.go
│ ├── chat_completions.go
│ ├── completions.go
│ ├── connection.go
│ ├── conversation/
│ │ ├── api.go
│ │ ├── conversation.go
│ │ ├── mask.go
│ │ ├── router.go
│ │ ├── shared.go
│ │ └── storage.go
│ ├── images.go
│ ├── manager.go
│ ├── relay.go
│ ├── router.go
│ ├── types.go
│ ├── usage.go
│ └── videos.go
├── middleware/
│ ├── auth.go
│ ├── builtins.go
│ ├── cors.go
│ ├── middleware.go
│ └── throttle.go
├── migration/
│ ├── 3.6.sql
│ └── 3.8.sql
├── nginx.conf
├── utils/
│ ├── base.go
│ ├── bootstrap.go
│ ├── buffer.go
│ ├── cache.go
│ ├── char.go
│ ├── compress.go
│ ├── config.go
│ ├── ctx.go
│ ├── encrypt.go
│ ├── fs.go
│ ├── image.go
│ ├── net.go
│ ├── scanner.go
│ ├── smtp.go
│ ├── sse.go
│ ├── templates/
│ │ └── code.html
│ ├── tokenizer.go
│ └── websocket.go
└── zeabur.yaml
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
app/node_modules
app/src-tauri
app/.idea
app/.vscode
app/dist
app/dev-dist
app/dist-ssr
app/target
app/tauri.conf.json
app/tauri.js
screenshot
.vscode
.idea
config.yaml
config.dev.yaml
# current in ~/storage
addition/generation/data/*
!addition/generation/data/.gitkeep
addition/article/data/*
!addition/article/data/.gitkeep
sdk
logs
chat
chat.exe
# for reverse engine
reverse
access.json
access/*.json
db
cache
config
README.md
.gitignore
screenshot
LICENSE
.github
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: 报告问题 | Bug Report
about: 使用简练详细的语言描述你遇到的问题 | Describe the issue you encountered in detail
title: ''
labels: bug
assignees: ''
---
[//]: # (方框内删除已有的空格,填 x 号)
+ [ ] 我已确认目前没有类似 issue
+ [ ] 我已确认我已升级到最新版本
+ [ ] 我已完整浏览项目 README 和项目文档并未找到解决方案
+ [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈
+ [ ] 我将以礼貌和尊重的态度提问,不得使用不文明用语 (包括在此发布评论的所有人同样适用, 不遵守的人将被 block)
+ [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭**
**问题描述**
**复现步骤**
**预期结果**
**日志信息**
**相关截图 (如果有)**
================================================
FILE: .github/ISSUE_TEMPLATE/channel_update.md
================================================
---
name: 渠道更新 | Channel Update
about: 新大模型供应商格式增加、更新请求 | Request to add or update a new llm provider format
title: ''
labels: feature
assignees: ''
---
[//]: # (方框内删除已有的空格,填 x 号)
+ [ ] 我已确认目前没有类似 issue
+ [ ] 我已确认我已升级到最新版本
+ [ ] 我已完整浏览项目 README 和项目文档并未找到解决方案
+ [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈
+ [ ] 我将以礼貌和尊重的态度提问,不得使用不文明用语 (包括在此发布评论的所有人同样适用, 不遵守的人将被 block)
+ [ ] 如果为新供应商格式,我已确认此供应商有一定的用户群体和知名度,借此以广告和推广类的名义的中转站点请求将被直接关闭
+ [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭**
**供应商名称**
**描述**
**供应商网址 / 截图 / 样例 (如果愿意提供)**
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Discord
url: https://discord.gg/rpzNSmqaF2
about: Join Discord Community
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: 功能请求 | Feature Request
about: 使用简练详细的语言描述希望加入的新功能 | Describe the feature you would like to request
title: ''
labels: feature
assignees: ''
---
[//]: # (方框内删除已有的空格,填 x 号)
+ [ ] 我已确认目前没有类似 issue
+ [ ] 我已确认我已升级到最新版本
+ [ ] 我已完整浏览项目 README 和项目文档并未找到解决方案
+ [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈
+ [ ] 我将以礼貌和尊重的态度提问,不得使用不文明用语 (包括在此发布评论的所有人同样适用, 不遵守的人将被 block)
+ [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭**
**功能描述**
**相关截图 (如果有)**
================================================
FILE: .github/workflows/app.yaml
================================================
name: Release App
on:
workflow_dispatch:
release:
types: [published]
jobs:
create-release:
permissions:
contents: write
runs-on: ubuntu-latest
outputs:
release_id: ${{ steps.create-release.outputs.result }}
steps:
- uses: actions/checkout@v3
- name: setup node
uses: actions/setup-node@v3
with:
node-version: 16
- name: get version
run: |
cd app
echo "PACKAGE_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').package.version")" >> $GITHUB_ENV
- name: create release
id: create-release
uses: actions/github-script@v6
with:
script: |
const { data } = await github.rest.repos.getLatestRelease({
owner: context.repo.owner,
repo: context.repo.repo,
})
return data.id
build-tauri:
needs: create-release
permissions:
contents: write
strategy:
fail-fast: false
matrix:
config:
- os: ubuntu-latest
arch: x86_64
rust_target: x86_64-unknown-linux-gnu
- os: macos-latest
arch: x86_64
rust_target: x86_64-apple-darwin
- os: macos-latest
arch: aarch64
rust_target: aarch64-apple-darwin
- os: windows-latest
arch: x86_64
rust_target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.config.os }}
steps:
- uses: actions/checkout@v3
- name: setup node
uses: actions/setup-node@v3
with:
node-version: 16
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.config.rust_target }}
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.config.rust_target }}
- name: install dependencies (ubuntu only)
if: matrix.config.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: install frontend dependencies
run: |
cd app
npm install -g pnpm
pnpm install
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
releaseId: ${{ needs.create-release.outputs.release_id }}
projectPath: ./app
publish-release:
permissions:
contents: write
runs-on: ubuntu-latest
needs: [create-release, build-tauri]
steps:
- name: publish release
id: publish-release
uses: actions/github-script@v6
env:
release_id: ${{ needs.create-release.outputs.release_id }}
with:
script: |
github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: process.env.release_id,
draft: false,
prerelease: false
})
================================================
FILE: .github/workflows/build.yaml
================================================
name: Build Test
on:
push:
branches:
- '*'
pull_request:
branches:
- '*'
jobs:
release:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [ 18.x ]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Use Golang
uses: actions/setup-go@v4
with:
go-version: '1.20'
- name: Build Backend
run: |
go build .
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Build Frontend
run: |
cd app
npm install -g pnpm
pnpm install
pnpm build
- name: Upload a Build Artifact
uses: actions/upload-artifact@v4.0.0
with:
name: Build result
path: app/dist
================================================
FILE: .github/workflows/docker-cd.yaml
================================================
name: Docker CD
on:
release:
types: [created]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build and push Docker images
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: programzmh/chatnio:stable
================================================
FILE: .github/workflows/docker-ci.yaml
================================================
name: Docker Image CI
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: "arm64,amd64"
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Get version
run: echo "VERSION=$(node -p "require('./app/src/conf/version.json').version")" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push Docker images
uses: docker/build-push-action@v3
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
programzmh/chatnio:latest
programzmh/chatnio:${{ env.VERSION }}
cache-from: |
type=registry,ref=programzmh/chatnio:buildcache
type=gha
cache-to: |
type=registry,ref=programzmh/chatnio:buildcache,mode=max
type=gha,mode=max
================================================
FILE: .github/workflows/issue-translator.yaml
================================================
name: Issue Translator
on:
issue_comment:
types: [created]
issues:
types: [opened]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: usthe/issues-translate-action@v2.7
with:
IS_MODIFY_TITLE: false
CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically.
================================================
FILE: .gitignore
================================================
app/node_modules
.vscode
.idea
config.yaml
config.dev.yaml
storage
addition/generation/data/*
!addition/generation/data/.gitkeep
addition/article/data/*
!addition/article/data/.gitkeep
sdk
logs
chat
*.exe
chat.exe
# for reverse engine
reverse
access.json
access/*.json
db
redis
config
presets
key/*
!key/.gitkeep
# for https://github.com/di-sukharev/opencommit
.env
================================================
FILE: Dockerfile
================================================
# Author: ProgramZmh
# License: Apache-2.0
# Description: Dockerfile for chatnio
FROM --platform=$TARGETPLATFORM golang:1.20-alpine AS backend
WORKDIR /backend
COPY . .
# Set go proxy to https://goproxy.cn (open for vps in China Mainland)
# RUN go env -w GOPROXY=https://goproxy.cn,direct
ARG TARGETARCH
ARG TARGETOS
ENV GOOS=$TARGETOS GOARCH=$TARGETARCH GO111MODULE=on CGO_ENABLED=1
# Install build dependencies
RUN apk update && \
apk add --no-cache \
gcc \
musl-dev \
g++ \
make \
linux-headers
# Build backend
RUN go build -o chat -a -ldflags="-extldflags=-static" .
FROM node:18 AS frontend
WORKDIR /app
COPY ./app .
RUN npm install -g pnpm && \
pnpm install && \
pnpm run build && \
rm -rf node_modules src
FROM alpine
# Install dependencies
RUN apk upgrade --no-cache && \
apk add --no-cache wget ca-certificates tzdata && \
update-ca-certificates 2>/dev/null || true
# Set timezone
RUN echo "Asia/Shanghai" > /etc/timezone && \
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
WORKDIR /
# Copy dist
COPY --from=backend /backend/chat /chat
COPY --from=backend /backend/config.example.yaml /config.example.yaml
COPY --from=backend /backend/utils/templates /utils/templates
COPY --from=backend /backend/addition/article/template.docx /addition/article/template.docx
COPY --from=frontend /app/dist /app/dist
# Volumes
VOLUME ["/config", "/logs", "/storage"]
# Expose port
EXPOSE 8094
# Run application
CMD ["./chat"]
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================

# [🥳 CoAI.Dev](https://coai.dev)
#### 🚀 Next Generation AIGC One-Stop Business Solution
#### *"CoAI.Dev > [Next Web](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) + [One API](https://github.com/songquanpeng/one-api)"*
English · [简体中文](./README_zh-CN.md) · [日本語](./README_ja-JP.md) · [Docs](https://coai.dev) · [Discord](https://discord.gg/rpzNSmqaF2) · [Deployment Guide](https://coai.dev/docs/deploy)
[](https://trendshift.io/repositories/6369)
## 📝 Features
1. 🤖️ **Rich Model Support**: Multi-model service provider support (OpenAI / Anthropic / Gemini / Midjourney and more than ten compatible formats & private LLM support)
2. 🤯 **Beautiful UI Design**: UI compatible with PC / Pad / Mobile, following [Shadcn UI](https://ui.shadcn.com) & [Tremor Charts](https://blocks.tremor.so) design standards, rich and beautiful interface design and backend dashboard
3. 🎃 **Complete Markdown Support**: Support for **LaTeX formulas** / **Mermaid mind maps** / table rendering / code highlighting / chart drawing / progress bars and other advanced Markdown syntax support
4. 👀 **Multi-theme Support**: Support for multiple theme switching, including **Light Mode** for light themes and **Dark Mode** for dark themes. 👉 [Custom Color Scheme](https://github.com/coaidev/coai/blob/main/app/src/assets/globals.less)
5. 📚 **Internationalization Support**: Support for internationalization, multi-language switching 🇨🇳 🇺🇸 🇯🇵 🇷🇺 👉 Welcome to contribute translations [Pull Request](https://github.com/coaidev/coai/pulls)
6. 🎨 **Text-to-Image Support**: Support for multiple text-to-image models: **OpenAI DALL-E**✅ & **Midjourney** (support for **U/V/R** operations)✅ & Stable Diffusion✅ etc.
7. 📡 **Powerful Conversation Sync**: **Zero-cost cross-device conversation sync support for users**, support for **conversation sharing** (link sharing & save as image & share management), **no need for WebDav / WebRTC and other dependencies and complex learning costs**
8. 🎈 **Model Market & Preset System**: Support for customizable model market in the backend, providing model introductions, tags, and other parameters. Site owners can customize model introductions according to the situation. Also supports a preset system, including **custom presets** and **cloud synchronization** functions.
9. 📖 **Rich File Parsing**: **Out-of-the-box**, supports file parsing for **all models** (PDF / Docx / Pptx / Excel / image formats parsing), **supports more cloud image storage solutions** (S3 / R2 / MinIO etc.), **supports OCR image recognition** 👉 See project [CoAI.Dev Blob Service](https://github.com/coaidev/blob-service) for details (supports Vercel / Docker one-click deployment)
10. 🌏 **Full Model Internet Search**: Based on the [SearXNG](https://github.com/searxng/searxng) open-source engine, supports rich search engines such as Google / Bing / DuckDuckGo / Yahoo / Wikipedia / Arxiv / Qwant, supports safe search mode, content truncation, image proxy, test search availability, and other functions.
11. 💕 **Progressive Web App (PWA)**: Supports PWA applications & desktop support (desktop based on [Tauri](https://github.com/tauri-apps/tauri))
12. 🤩 **Comprehensive Backend Management**: Supports beautiful and rich dashboard, announcement & notification management, user management, subscription management, gift code & redemption code management, price setting, subscription setting, custom model market, custom site name & logo, SMTP email settings, and other functions
13. 🤑 **Multiple Billing Methods**: Supports 💴 **Subscription** and 💴 **Elastic Billing** two billing methods. Elastic billing supports per-request billing / token billing / no billing / anonymous calls and **minimum request points** detection and other powerful features
14. 🎉 **Innovative Model Caching**: Supports enabling model caching: i.e., under the same request parameter hash, if it has been requested before, it will directly return the cached result (hitting the cache will not be billed), reducing the number of requests. You can customize whether to cache models, cache time, multiple cache result numbers, and other advanced cache settings
15. 🥪 **Additional Features** (Support Discontinued): 🍎 **AI Project Generator Function** / 📂 **Batch Article Generation Function** / 🥪 **AI Card Function** (Deprecated)
16. 😎 **Excellent Channel Management**: Self-written excellent channel algorithm, supports ⚡ **multi-channel management**, supports 🥳**priority** setting for channel call order, supports 🥳**weight** setting for load balancing probability distribution of channels at the same priority, supports 🥳**user grouping**, 🥳**automatic retry on failure**, 🥳**model redirection**, 🥳**built-in upstream hiding**, 🥳**channel status management** and other powerful **enterprise-level functions**
17. ⭐ **OpenAI API Distribution & Proxy System**: Supports calling various large models in **OpenAI API** standard format, integrates powerful channel management functions, only needs to deploy one site to achieve simultaneous development of B/C-end business💖
18. 👌 **Quick Upstream Synchronization**: Channel settings, model market, price settings, and other settings can quickly synchronize with upstream sites, modify your site configuration based on this, quickly build your site, save time and effort, one-click synchronization, quick launch
19. 👋 **SEO Optimization**: Supports SEO optimization, supports custom site name, site logo, and other SEO optimization settings to make search engines crawl faster, making your site stand out👋
20. 🎫 **Multiple Redemption Code Systems**: Supports multiple redemption code systems, supports gift codes and redemption codes, supports batch generation, gift codes are suitable for promotional distribution, redemption codes are suitable for card sales, for gift codes of one type, a user can only redeem one code, which to some extent reduces the situation of one user redeeming multiple times in promotions😀
21. 🥰 **Business-Friendly License**: Adopts the **Apache-2.0** open-source license, friendly for commercial secondary development & distribution (please also comply with the provisions of the Apache-2.0 license, do not use for illegal purposes)
> ### ✨ CoAI.Dev Business
>
> 
>
> - ✅ Beautiful commercial-grade UI, elegant frontend interface and backend management
> - ✅ Supports TTS & STT, plugin marketplace, RAG knowledge base and other rich features and modules
> - ✅ More payment providers, more billing models and advanced order management
> - ✅ Supports more authentication methods, including SMS login, OAuth login, etc.
> - ✅ Supports model monitoring, channel health detection, fault alarm automatic channel switching
> - ✅ Supports multi-tenant API Key distribution system, enterprise-level token permission management and visitor restrictions
> - ✅ Supports security auditing, logging, model rate limiting, API Gateway and other advanced features
> - ✅ Supports promotion rewards, professional data statistics, user profile analysis and other business analysis capabilities
> - ✅ Supports Discord/Telegram/Feishu and other bot integration capabilities (extension modules)
> - ...
>
> [👉 Learn More](https://www.coai.dev/docs/contact)
## 🔨 Supported Models
1. OpenAI & Azure OpenAI *(✅ Vision ✅ Function Calling)*
2. Anthropic Claude *(✅ Vision ✅ Function Calling)*
3. Google Gemini & PaLM2 *(✅ Vision)*
4. Midjourney *(✅ Mode Toggling ✅ U/V/R Actions)*
5. iFlytek SparkDesk *(✅ Vision ✅ Function Calling)*
6. Zhipu AI ChatGLM *(✅ Vision)*
7. Alibaba Tongyi Qwen
8. Tencent Hunyuan
9. Baichuan AI
10. Moonshot AI (👉 OpenAI)
11. DeepSeek AI (👉 OpenAI)
12. ByteDance Skylark *(✅ Function Calling)*
13. Groq Cloud AI
14. OpenRouter (👉 OpenAI)
15. 360 GPT
16. LocalAI / Ollama (👉 OpenAI)
## 👻 OpenAI Compatible API Proxy
- [x] Chat Completions _(/v1/chat/completions)_
- [x] Image Generation _(/v1/images)_
- [x] Model List _(/v1/models)_
- [x] Dashboard Billing _(/v1/billing)_
## 📦 Deployment
> [!TIP]
> **After successful deployment, the admin account is `root`, with the default password `chatnio123456`**
### ✨ Zeabur (One-Click)
[](https://zeabur.com/templates/M86XJI)
> Zeabur provides a certain free quota, you can use non-paid regions for one-click deployment, and also supports plan subscriptions and elastic billing for flexible expansion.
> 1. Click `Deploy` to deploy, and enter the domain name you wish to bind, wait for the deployment to complete.
> 2. After deployment is complete, please visit your domain name and log in to the backend management using the username `root` and password `chatnio123456`. Please follow the prompts to change the password in the chatnio backend in a timely manner.
### 🐳 BTPanel (One-Click)
1. Install Baota Panel by visiting [BTPanel](https://www.bt.cn/new/download.html) and install using the stable version script.
2. Log in to the panel and click **Docker** on the left to enter Docker management.
3. If prompted that Docker / Docker Compose is not installed, you can install according to the guide above.
4. After installation is complete, enter **App Store**, search for `CoAI` and click **Install**.
5. Configure basic application information such as your domain name, port, etc., and click **Confirm** (default configuration can be used).
6. First-time installation may take 1-2 minutes to complete database initialization. If you encounter issues, please check the panel running logs for troubleshooting.
7. Visit your configured domain or server `http://[ip]:[port]`, log in to the backend management using username `root` and password `chatnio123456`.
### AlibabaCloud ComputeNest (One-Click)
[](https://computenest.console.aliyun.com/service/instance/create/ap-southeast-1?type=user&ServiceName=CoAI%20%20Community%20Edition)
1. Access the CoAI service on [ComputeNest International Edition](https://computenest.console.aliyun.com/service/instance/create/ap-southeast-1?type=user&ServiceName=CoAI%20%20Community%20Edition). If you are in China, please visit [ComputeNest](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=CoAI社区版), and fill in the deployment parameters as prompted.
2. Select the payment type, fill in the instance parameters and network parameters, and click **Next: Confirm Order**.
3. After confirming the deployment parameters and checking the estimated price, click Create Now and wait for the service instance to be deployed.
4. Click **Service Instance** on the left. After the service instance is deployed, click the instance ID to enter the details interface.
5. Click the address in **Use Now** on the details interface to enter the CoAI interface. The default username is `root` and the password is `chatnio123456` to log in to the backend management.
6. For more operation details and payment information, see:[Service Details](https://computenest.console.aliyun.com/service/detail/ap-southeast-1/service-27e11d3a5c9b40628505/1?type=user&isRecommend=true).
### ⚡ Docker Compose Installation (Recommended)
> [!NOTE]
> After successful execution, the host machine mapping address is `http://localhost:8000`
```shell
git clone --depth=1 --branch=main --single-branch https://github.com/coaidev/coai.git
cd chatnio
docker-compose up -d # Run the service
# To use the stable version, use docker-compose -f docker-compose.stable.yaml up -d instead
# To use Watchtower for automatic updates, use docker-compose -f docker-compose.watch.yaml up -d instead
```
Version update (_If Watchtower automatic updates are enabled, manual updates are not necessary_):
```shell
docker-compose down
docker-compose pull
docker-compose up -d
```
> - MySQL database mount directory: ~/**db**
> - Redis database mount directory: ~/**redis**
> - Configuration file mount directory: ~/**config**
### ⚡ Docker Installation (Lightweight runtime, commonly used for external _MYSQL/RDS_ services)
> [!NOTE]
> After successful execution, the host machine address is `http://localhost:8094`.
>
> To use the stable version, use `programzmh/chatnio:stable` instead of `programzmh/chatnio:latest`
```shell
docker run -d --name chatnio \
--network host \
-v ~/config:/config \
-v ~/logs:/logs \
-v ~/storage:/storage \
-e MYSQL_HOST=localhost \
-e MYSQL_PORT=3306 \
-e MYSQL_DB=chatnio \
-e MYSQL_USER=root \
-e MYSQL_PASSWORD=chatnio123456 \
-e REDIS_HOST=localhost \
-e REDIS_PORT=6379 \
-e SECRET=secret \
-e SERVE_STATIC=true \
programzmh/chatnio:latest
```
> - *--network host* means using the host machine's network, allowing the Docker container to use the host's network. You can modify this as needed.
> - SECRET: JWT secret key, generate a random string and modify accordingly
> - SERVE_STATIC: Whether to enable static file serving (normally this doesn't need to be changed, see FAQ below for details)
> - *-v ~/config:/config* mounts the configuration file, *-v ~/logs:/logs* mounts the host machine directory for log files, *-v ~/storage:/storage* mounts the directory for additional feature generated files
> - MySQL and Redis services need to be configured. Please refer to the information above to modify the environment variables accordingly
Version update (_After enabling Watchtower, manual updates are not necessary. After execution, follow the steps above to run again_):
```shell
docker stop chatnio
docker rm chatnio
docker pull programzmh/chatnio:latest
```
### ⚒ Compile and Install
> [!NOTE]
> After successful deployment, the default port is **8094**, and the access address is `http://localhost:8094`
>
> Config settings (~/config/**config.yaml**) can be overridden using environment variables. For example, the `MYSQL_HOST` environment variable can override the `mysql.host` configuration item
```shell
git clone https://github.com/coaidev/coai.git
cd chatnio
cd app
npm install -g pnpm
pnpm install
pnpm build
cd ..
go build -o chatnio
# e.g. using nohup (you can also use systemd or other service manager)
nohup ./chatnio > output.log & # using nohup to run in background
```
## 📦 Tech Stack
- 🥗 Frontend: React + Redux + Radix UI + Tailwind CSS
- 🍎 Backend: Golang + Gin + Redis + MySQL
- 🍒 Application Technology: PWA + WebSocket
## 🤯 Why Create This Project & Project Advantages
- We found that most AIGC commercial sites on the market are frontend-oriented lightweight deployment projects with beautiful UI interface designs, such as the commercial version of [Next Chat](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web). Due to its personal privatization-oriented design, there are some limitations in secondary commercial development, presenting some issues, such as:
1. **Difficult conversation synchronization**, for example, requiring services like WebDav, high user learning costs, and difficulties in real-time cross-device synchronization.
2. **Insufficient billing**, for example, only supporting elastic billing or only subscription-based, unable to meet the needs of different users.
3. **Inconvenient file parsing**, for example, only supporting uploading images to an image hosting service first, then returning to the site to input the URL direct link in the input box, without built-in file parsing functionality.
4. **No support for conversation URL sharing**, for example, only supporting conversation screenshot sharing, unable to support conversation URL sharing (or only supporting tools like ShareGPT, which cannot promote the site).
5. **Insufficient channel management**, for example, the backend only supports OpenAI format channels, making it difficult to be compatible with other format channels. And only one channel can be filled in, unable to support multi-channel management.
6. **No API call support**, for example, only supporting user interface calls, unable to support API proxying and management.
- Another type is API distribution-oriented sites with powerful distribution systems, such as projects based on [One API](https://github.com/songquanpeng/one-api).
Although these projects support powerful API proxying and management, they lack interface design and some C-end features, such as:
1. **Insufficient user interface**, for example, only supporting API calls, without built-in user interface chat. User interface chat requires manually copying the key and going to other sites to use, which has a high learning cost for ordinary users.
2. **No subscription system**, for example, only supporting elastic billing, lacking billing design for C-end users, unable to meet different user needs, and not user-friendly in terms of cost perception for users without a foundation.
3. **Insufficient C-end features**, for example, only supporting API calls, not supporting conversation synchronization, conversation sharing, file parsing, and other functions.
4. **Insufficient load balancing**, the open-source version does not support the **weight** parameter, unable to achieve balanced load distribution probability for channels at the same priority ([New API](https://github.com/Calcium-Ion/new-api) also solves this pain point, with a more beautiful UI).
Therefore, we hope to combine the advantages of these two types of projects to create a project that has both a powerful API distribution system and a rich user interface design,
thus meeting the needs of C-end users while developing B-end business, improving user experience, reducing user learning costs, and increasing user stickiness.
Thus, **CoAI.Dev** was born. We hope to create a project that has both a powerful API distribution system and a rich user interface design, becoming the next-generation open-source AIGC project's one-stop commercial solution.
## ❤ Donations
If you find this project helpful, you can give it a Star to show your support!
================================================
FILE: README_ja-JP.md
================================================

# [🥳 CoAI.Dev](https://coai.dev)
#### 🚀 次世代AIGCワンストップビジネスソリューション
#### *"CoAI.Dev > [Next Web](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) + [One API](https://github.com/songquanpeng/one-api)"*
English · [简体中文](./README_zh-CN.md) · [日本語](./README_ja-JP.md) · [ドキュメント](https://coai.dev) · [Discord](https://discord.gg/rpzNSmqaF2) · [デプロイガイド](https://coai.dev/docs/deploy)
[](https://trendshift.io/repositories/6369)
## 📝 機能
1. 🤖️ **豊富なモデルサポート**: 複数のモデルサービスプロバイダーのサポート (OpenAI / Anthropic / Gemini / Midjourney など十数種類の互換フォーマット & プライベートLLMサポート)
2. 🤯 **美しいUIデザイン**: PC / Pad / モバイルに対応したUI、[Shadcn UI](https://ui.shadcn.com) & [Tremor Charts](https://blocks.tremor.so) のデザイン基準に従った豊富で美しいインターフェースデザインとバックエンドダッシュボード
3. 🎃 **完全なMarkdownサポート**: **LaTeX数式** / **Mermaidマインドマップ** / テーブルレンダリング / コードハイライト / チャート描画 / プログレスバーなどの高度なMarkdown構文サポート
4. 👀 **マルチテーマサポート**: 複数のテーマ切り替えをサポート、ライトテーマの**ライトモード**とダークテーマの**ダークモード**を含む。 👉 [カスタムカラースキーム](https://github.com/coaidev/coai/blob/main/app/src/assets/globals.less)
5. 📚 **国際化サポート**: 国際化をサポートし、複数の言語切り替えをサポート 🇨🇳 🇺🇸 🇯🇵 🇷🇺 👉 翻訳の貢献を歓迎します [Pull Request](https://github.com/coaidev/coai/pulls)
6. 🎨 **テキストから画像へのサポート**: 複数のテキストから画像へのモデルをサポート: **OpenAI DALL-E**✅ & **Midjourney** ( **U/V/R** 操作をサポート)✅ & Stable Diffusion✅ など
7. 📡 **強力な会話同期**: **ユーザーのゼロコストクロスデバイス会話同期サポート**、**会話共有** (リンク共有 & 画像として保存 & 共有管理) をサポート、**WebDav / WebRTCなどの依存関係や複雑な学習コストは不要**
8. 🎈 **モデル市場 & プリセットシステム**: バックエンドでカスタマイズ可能なモデル市場をサポートし、モデルの紹介、タグなどのパラメータを提供。サイトオーナーは状況に応じてモデルの紹介をカスタマイズできます。また、**カスタムプリセット**と**クラウド同期**機能を含むプリセットシステムもサポート。
9. 📖 **豊富なファイル解析**: **すぐに使える**、**すべてのモデル**のファイル解析をサポート (PDF / Docx / Pptx / Excel / 画像形式の解析)、**より多くのクラウド画像ストレージソリューション** (S3 / R2 / MinIO など) をサポート、**OCR画像認識**をサポート 👉 詳細はプロジェクト [CoAI.Dev Blob Service](https://github.com/coaidev/blob-service) を参照 (Vercel / Dockerのワンクリックデプロイをサポート)
10. 🌏 **全モデルインターネット検索**: [SearXNG](https://github.com/searxng/searxng) オープンソースエンジンに基づき、Google / Bing / DuckDuckGo / Yahoo / Wikipedia / Arxiv / Qwant などの豊富な検索エンジン検索をサポート、安全検索モード、コンテンツ切り捨て、画像プロキシ、検索可用性テストなどの機能をサポート。
11. 💕 **プログレッシブウェブアプリ (PWA)**: PWAアプリケーションをサポートし、デスクトップサポート (デスクトップは [Tauri](https://github.com/tauri-apps/tauri) に基づく)
12. 🤩 **包括的なバックエンド管理**: 美しく豊富なダッシュボード、公告 & 通知管理、ユーザー管理、サブスクリプション管理、ギフトコード & 交換コード管理、価格設定、サブスクリプション設定、カスタムモデル市場、カスタムサイト名 & ロゴ、SMTPメール設定などの機能をサポート
13. 🤑 **複数の課金方法**: 💴 **サブスクリプション** と 💴 **エラスティック課金** の2つの課金方法をサポート。エラスティック課金は、リクエストごとの課金 / トークン課金 / 無課金 / 匿名通話をサポートし、**最小リクエストポイント** 検出などの強力な機能をサポート
14. 🎉 **革新的なモデルキャッシュ**: モデルキャッシュの有効化をサポート: 同じリクエストパラメータハッシュの下で、以前にリクエストされた場合、キャッシュ結果を直接返します (キャッシュヒットは課金されません)、リクエスト回数を減らします。キャッシュするモデル、キャッシュ時間、複数のキャッシュ結果数などの高度なキャッシュ設定をカスタマイズできます
15. 🥪 **追加機能** (サポート終了): 🍎 **AIプロジェクトジェネレーター機能** / 📂 **バッチ記事生成機能** / 🥪 **AIカード機能** (廃止)
16. 😎 **優れたチャネル管理**: 自作の優れたチャネルアルゴリズム、⚡ **マルチチャネル管理** をサポート、チャネル呼び出し順序の設定をサポート、同じ優先度のチャネルの負荷分散確率の設定をサポート、**ユーザーグループ化**、**失敗時の自動再試行**、**モデルリダイレクト**、**組み込みの上流非表示**、**チャネル状態管理** などの強力な**企業レベルの機能**をサポート
17. ⭐ **OpenAI API分散 & プロキシシステム**: **OpenAI API** 標準フォーマットでさまざまな大規模モデルを呼び出すことをサポートし、強力なチャネル管理機能を統合。1つのサイトをデプロイするだけで、B/Cエンドビジネスの同時開発を実現💖
18. 👌 **迅速な上流同期**: チャネル設定、モデル市場、価格設定などの設定を迅速に上流サイトと同期し、これに基づいてサイト設定を変更し、迅速にサイトを構築し、時間と労力を節約し、ワンクリック同期、迅速なオンライン化を実現
19. 👋 **SEO最適化**: SEO最適化をサポートし、カスタムサイト名、サイトロゴなどのSEO最適化設定をサポートし、検索エンジンがより速くクロールできるようにし、サイトを際立たせます👋
20. 🎫 **複数の交換コードシステム**: 複数の交換コードシステムをサポートし、ギフトコードと交換コードをサポートし、バッチ生成をサポート。ギフトコードはプロモーション配布に適しており、交換コードはカード販売に適しています。ギフトコードの1つのタイプの複数のコードは、1人のユーザーが1つのコードしか交換できないため、プロモーション中に1人のユーザーが複数回交換する状況をある程度減らします😀
21. 🥰 **ビジネスフレンドリーなライセンス**: **Apache-2.0** オープンソースライセンスを採用し、商用の二次開発 & 配布にフレンドリー (Apache-2.0ライセンスの規定を遵守し、違法な目的で使用しないでください)
> ### ✨ CoAI.Dev ビジネス版
>
> 
>
> - ✅ 美しい商用グレードのUI、エレガントなフロントエンドインターフェースとバックエンド管理
> - ✅ TTS & STT、プラグインマーケットプレイス、RAGナレッジベースなどの豊富な機能とモジュールをサポート
> - ✅ より多くの支払いプロバイダー、より多くの課金モデルと高度な注文管理をサポート
> - ✅ SMSログイン、OAuthログインなど、より多くの認証方法をサポート
> - ✅ モデル監視、チャネルの健康状態検出、障害アラーム自動チャネル切り替えをサポート
> - ✅ マルチテナントAPIキー配布システム、企業レベルのトークン権限管理と訪問者制限をサポート
> - ✅ セキュリティ監査、ログ記録、モデルレート制限、APIゲートウェイなどの高度な機能をサポート
> - ✅ プロモーション報酬、プロフェッショナルなデータ統計、ユーザープロファイル分析などのビジネス分析能力をサポート
> - ✅ Discord/Telegram/Feishuなどのボット統合機能をサポート (拡張モジュール)
> - ...
>
> [👉 詳細はこちら](https://www.coai.dev/docs/contact)
## 🔨 サポートされているモデル
1. OpenAI & Azure OpenAI *(✅ Vision ✅ Function Calling)*
2. Anthropic Claude *(✅ Vision ✅ Function Calling)*
3. Google Gemini & PaLM2 *(✅ Vision)*
4. Midjourney *(✅ Mode Toggling ✅ U/V/R Actions)*
5. iFlytek SparkDesk *(✅ Vision ✅ Function Calling)*
6. Zhipu AI ChatGLM *(✅ Vision)*
7. Alibaba Tongyi Qwen
8. Tencent Hunyuan
9. Baichuan AI
10. Moonshot AI (👉 OpenAI)
11. DeepSeek AI (👉 OpenAI)
12. ByteDance Skylark *(✅ Function Calling)*
13. Groq Cloud AI
14. OpenRouter (👉 OpenAI)
15. 360 GPT
16. LocalAI / Ollama (👉 OpenAI)
## 👻 OpenAI互換APIプロキシ
- [x] Chat Completions _(/v1/chat/completions)_
- [x] Image Generation _(/v1/images)_
- [x] Model List _(/v1/models)_
- [x] Dashboard Billing _(/v1/billing)_
## 📦 デプロイ
> [!TIP]
> **デプロイが成功した後、管理者アカウントは `root` で、デフォルトのパスワードは `chatnio123456` です**
### ✨ Zeabur (ワンクリック)
[](https://zeabur.com/templates/M86XJI)
> Zeaburは一定の無料クォータを提供しており、非有料地域でのワンクリックデプロイをサポートし、プランサブスクリプションとエラスティック課金による柔軟な拡張もサポートしています。
> 1. `Deploy` をクリックしてデプロイし、バインドしたいドメイン名を入力し、デプロイが完了するのを待ちます。
> 2. デプロイが完了したら、ドメイン名にアクセスし、ユーザー名 `root` とパスワード `chatnio123456` を使用してバックエンド管理にログインします。チャットニオのバックエンドでパスワードを変更するように指示に従ってください。
### ⚡ Docker Composeインストール (推奨)
> [!NOTE]
> 実行が成功した後、ホストマシンのマッピングアドレスは `http://localhost:8000` です
```shell
git clone --depth=1 --branch=main --single-branch https://github.com/coaidev/coai.git
cd chatnio
docker-compose up -d # サービスを実行
# 安定版を使用するには、docker-compose -f docker-compose.stable.yaml up -d を使用してください
# Watchtowerを使用して自動更新するには、docker-compose -f docker-compose.watch.yaml up -d を使用してください
```
バージョン更新 (_Watchtower自動更新が有効な場合、手動更新は不要です_):
```shell
docker-compose down
docker-compose pull
docker-compose up -d
```
> - MySQLデータベースマウントディレクトリ: ~/**db**
> - Redisデータベースマウントディレクトリ: ~/**redis**
> - 設定ファイルマウントディレクトリ: ~/**config**
### ⚡ Dockerインストール (軽量ランタイム、外部 _MYSQL/RDS_ サービスでよく使用されます)
> [!NOTE]
> 実行が成功した後、ホストマシンのアドレスは `http://localhost:8094` です。
>
> 安定版を使用するには、`programzmh/chatnio:stable` を `programzmh/chatnio:latest` の代わりに使用してください
```shell
docker run -d --name chatnio \
--network host \
-v ~/config:/config \
-v ~/logs:/logs \
-v ~/storage:/storage \
-e MYSQL_HOST=localhost \
-e MYSQL_PORT=3306 \
-e MYSQL_DB=chatnio \
-e MYSQL_USER=root \
-e MYSQL_PASSWORD=chatnio123456 \
-e REDIS_HOST=localhost \
-e REDIS_PORT=6379 \
-e SECRET=secret \
-e SERVE_STATIC=true \
programzmh/chatnio:latest
```
> - *--network host* はホストマシンのネットワークを使用することを意味し、Dockerコンテナがホストのネットワークを使用できるようにします。必要に応じて変更できます。
> - SECRET: JWTシークレットキー、ランダムな文字列を生成して適宜変更
> - SERVE_STATIC: 静的ファイルの提供を有効にするかどうか (通常、これを変更する必要はありません。詳細はFAQを参照)
> - *-v ~/config:/config* は設定ファイルをマウントし、*-v ~/logs:/logs* はログファイルのホストマシンディレクトリをマウントし、*-v ~/storage:/storage* は追加機能の生成ファイルのディレクトリをマウント
> - MySQLとRedisサービスを設定する必要があります。上記の情報を参照して環境変数を適宜変更してください
バージョン更新 (_Watchtowerを有効にした後、手動更新は不要です。実行後、上記の手順に従って再度実行してください_):
```shell
docker stop chatnio
docker rm chatnio
docker pull programzmh/chatnio:latest
```
### ⚒ コンパイルとインストール
> [!NOTE]
> デプロイが成功した後、デフォルトのポートは **8094** で、アクセスアドレスは `http://localhost:8094` です
>
> 設定項目 (~/config/**config.yaml**) は環境変数を使用して上書きできます。例えば、`MYSQL_HOST` 環境変数は `mysql.host` 設定項目を上書きできます
```shell
git clone https://github.com/coaidev/coai.git
cd chatnio
cd app
npm install -g pnpm
pnpm install
pnpm build
cd ..
go build -o chatnio
# 例: nohupを使用 (systemdや他のサービスマネージャーも使用できます)
nohup ./chatnio > output.log & # バックグラウンドで実行するためにnohupを使用
```
## 📦 技術スタック
- 🥗 フロントエンド: React + Redux + Radix UI + Tailwind CSS
- 🍎 バックエンド: Golang + Gin + Redis + MySQL
- 🍒 アプリケーション技術: PWA + WebSocket
## 🤯 なぜこのプロジェクトを作成したのか & プロジェクトの利点
- 市場にあるほとんどのAIGC商用サイトは、[Next Chat](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) の商用版のように、美しいUIインターフェースデザインを持つフロントエンド指向の軽量デプロイプロジェクトです。個人のプライベート化指向の設計のため、二次商用開発にはいくつかの制限があり、いくつかの問題が発生します。例えば:
1. **会話の同期が難しい**: 例えば、WebDavなどのサービスが必要で、ユーザーの学習コストが高く、クロスデバイスのリアルタイム同期が困難です。
2. **課金が不十分**: 例えば、エラスティック課金のみをサポートするか、サブスクリプションのみをサポートし、異なるユーザーのニーズを満たすことができません。
3. **ファイル解析が不便**: 例えば、最初に画像ホスティングサービスに画像をアップロードし、サイトに戻って入力ボックスにURL直リンクを入力する必要があり、組み込みのファイル解析機能がありません。
4. **会話URL共有のサポートがない**: 例えば、会話のスクリーンショット共有のみをサポートし、会話URL共有をサポートしません (またはShareGPTなどのツールのみをサポートし、サイトのプロモーションには役立ちません)。
5. **チャネル管理が不十分**: 例えば、バックエンドはOpenAIフォーマットのチャネルのみをサポートし、他のフォーマットのチャネルとの互換性が難しいです。そして、1つのチャネルしか入力できず、マルチチャネル管理をサポートしません。
6. **API呼び出しのサポートがない**: 例えば、ユーザーインターフェース呼び出しのみをサポートし、APIプロキシと管理をサポートしません。
- もう1つのタイプは、[One API](https://github.com/songquanpeng/one-api) に基づくプロジェクトのように、強力な分散システムを持つAPI分散指向のサイトです。
これらのプロジェクトは強力なAPIプロキシと管理をサポートしていますが、インターフェースデザインといくつかのCエンド機能が不足しています。例えば:
1. **ユーザーインターフェースが不十分**: 例えば、API呼び出しのみをサポートし、組み込みのユーザーインターフェースチャットがありません。ユーザーインターフェースチャットは、手動でキーをコピーして他のサイトに行って使用する必要があり、普通のユーザーにとって学習コストが高いです。
2. **サブスクリプションシステムがない**: 例えば、エラスティック課金のみをサポートし、Cエンドユーザーの課金設計が不足しており、異なるユーザーのニーズを満たすことができず、基礎のないユーザーにとってコスト感知がフレンドリーではありません。
3. **Cエンド機能が不十分**: 例えば、API呼び出しのみをサポートし、会話同期、会話共有、ファイル解析などの機能をサポートしません。
4. **負荷分散が不十分**: オープンソース版は**重み**パラメータをサポートしておらず、同じ優先度のチャネルの負荷分散確率を実現できません ([New API](https://github.com/Calcium-Ion/new-api) もこの痛点を解決し、UIもより美しいです)。
したがって、これら2つのタイプのプロジェクトの利点を組み合わせて、強力なAPI分散システムと豊富なユーザーインターフェースデザインを持つプロジェクトを作成し、
Cエンドユーザーのニーズを満たしながらBエンドビジネスを開発し、ユーザーエクスペリエンスを向上させ、ユーザーの学習コストを削減し、ユーザーの粘着性を高めることを目指しています。
そのため、**CoAI.Dev** が誕生しました。強力なAPI分散システムと豊富なユーザーインターフェースデザインを持つプロジェクトを作成し、次世代のオープンソースAIGCプロジェクトのワンストップ商用ソリューションになることを目指しています。
## ❤ 寄付
このプロジェクトが役立つと思われる場合は、Starをクリックしてサポートを示すことができます!
================================================
FILE: README_zh-CN.md
================================================

# [🥳 CoAI.Dev](https://coai.dev)
#### 🚀 下一代 AIGC 一站式商业解决方案
#### *“ CoAI.Dev > [Next Web](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) + [One API](https://github.com/songquanpeng/one-api) ”*
[English](./README.md) · 简体中文 · [文档](https://coai.dev) · [Discord](https://discord.gg/rpzNSmqaF2) · [部署文档](https://coai.dev/docs/deploy)
[](https://trendshift.io/repositories/6369)
## 📝 功能
1. 🤖️ **丰富模型支持**: 多模型服务商支持 (OpenAI / Anthropic / Gemini / Midjourney 等十余种格式兼容 & 私有化 LLM 支持)
2. 🤯 **美观 UI 设计**: UI 兼容 PC / Pad / 移动三端,遵循 [Shadcn UI](https://ui.shadcn.com) & [Tremor Charts](https://blocks.tremor.so) 设计规范,丰富美观的界面设计和后台仪表盘
3. 🎃 **完整 Markdown 支持**: 支持 **LaTeX 公式** / **Mermaid 思维导图** / 表格渲染 / 代码高亮 / 图表绘制 / 进度条等进阶 Markdown 语法支持
4. 👀 **多主题支持**: 支持多种主题切换,包含亮色主题的**明亮模式**和暗色主题的**深色模式**。 👉 [自定义配色](https://github.com/coaidev/coai/blob/main/app/src/assets/globals.less)
5. 📚 **国际化支持**: 支持国际化,支持多语言切换 🇨🇳 🇺🇸 🇯🇵 🇷🇺 👉 欢迎贡献翻译 [Pull Request](https://github.com/coaidev/coai/pulls)
6. 🎨 **文生图支持**: 支持多种文生图模型: **OpenAI DALL-E**✅ & **Midjourney** (支持 **U/V/R** 操作)✅ & Stable Diffusion✅ 等
7. 📡 **强大对话同步**: **用户 0 成本对话跨端同步支持**,支持**对话分享** (支持链接分享 & 保存为图片 & 分享管理), **无需 WebDav / WebRTC 等依赖和复杂学习成本**
8. 🎈 **模型市场 & 预设系统**: 支持后台可自定义的模型市场, 可提供模型介绍、标签等参数, 站长可根据情况自定义模型简介。同时支持预设系统,包含 **自定义预设** 和 **云端同步** 功能。
9. 📖 **丰富文件解析**: **开箱即用**, 支持**所有模型**的文件解析 (PDF / Docx / Pptx / Excel / 图片等格式解析), **支持更多云端图片存储方案** (S3 / R2 / MinIO 等), **支持 OCR 图片识别** 👉 详情参见项目 [CoAI.Dev Blob Service](https://github.com/coaidev/blob-service) (支持 Vercel / Docker 一键部署)
10. 🌏 **全模型联网搜索**: 基于 [SearXNG](https://github.com/searxng/searxng) 开源引擎, 支持 Google / Bing / DuckDuckGo / Yahoo / WikiPedia / Arxiv / Qwant 等丰富搜索引擎搜索, 支持安全搜索模式, 内容截断, 图片代理, 测试搜索可用性等功能。
11. 💕 **渐进式 Web 应用 (PWA)**: 支持 PWA 应用 & 支持桌面端 (桌面端基于 [Tauri](https://github.com/tauri-apps/tauri))
12. 🤩 **齐全后台管理**: 支持美观丰富的仪表盘, 公告 & 通知管理, 用户管理, 订阅管理, 礼品码 & 兑换码管理, 价格设定, 订阅设定, 自定义模型市场, 自定义站点名称 & Logo, SMTP 发件设置等功能
13. 🤑 **多种计费方式**: 支持 💴 **订阅制** 和 💴 **弹性计费** 两种计费方式, 弹性计费支持 次数计费 / Token 计费 / 不计费 / 可匿名调用 和 **最小请求点数** 检测等强大功能
14. 🎉 **创新模型缓存**: 支持开启模型缓存:即同一个请求入参 Hash 下, 如果之前已请求过, 将直接返回缓存结果 (击中缓存将不计费), 减少请求次数。可自行自定义是否缓存的模型、缓存时间、多种缓存结果数等高级缓存设置
15. 🥪 **附加功能** (停止支持): 🍎 **AI 项目生成器功能** / 📂 **批量文章生成功能** / 🥪 **AI 卡片功能** (已废弃)
16. 😎 **优秀渠道管理**: 自写优秀渠道算法, 支持⚡ **多渠道管理**, 支持🥳**优先级**设置渠道的调用顺序, 支持🥳**权重**设置同一优先级下的渠道均衡负载分配概率, 支持🥳**用户分组**, 🥳**失败自动重试**, 🥳**模型重定向**, 🥳**内置上游隐藏**, 🥳**渠道状态管理**等强大**企业级功能**
17. ⭐ **OpenAI API 分发 & 中转系统**: 支持以 **OpenAI API** 标准格式调用各种大模型, 集成强大的渠道管理功能, 仅需部署一个站点即可实现同时发展 B/C 端业务💖
18. 👌 **快速同步上游**: 渠道设置、模型市场、价格设定等设置都可快速同步上游站点,以此基础修改自己的站点配置,快速搭建自己的站点,省时省力,一键同步,快速上线
19. 👋 **SEO 优化**: 支持 SEO 优化,支持自定义站点名称、站点 Logo 等 SEO 优化设设置使搜索引擎更快的爬取,你的站点与众不同👋
20. 🎫 **多种兑换码体系**: 支持多种兑换码体系,支持礼品码和兑换码,支持批量生成,礼品码适合宣传分发,兑换码适合发卡销售,礼品码一个类型的多个码一个用户仅能兑换一个码,在宣传中一定程度上减少一个用户兑换多次的情况😀
21. 🥰 **商用友好协议**: 采用 **Apache-2.0** 开源协议, 商用二开 & 分发友好 (也请遵守 Apache-2.0 协议的规定, 请勿用于违法用途)
> ### ✨ CoAI.Dev 商业版
> 
>
> - ✅ 美观商业级 UI, 漂亮的前端界面与后台管理
> - ✅ 支持 TTS & STT, 插件市场, RAG 知识库等丰富功能与模块
> - ✅ 更多支付供应商, 更多计费模式和高级订单管理
> - ✅ 支持更多鉴权方式,包括短信登录、OAuth 登录等
> - ✅ 支持模型监控,渠道健康检测,故障告警自动渠道切换
> - ✅ 支持多租户 API Key 分发系统, 企业级令牌权限管理与访问者限制
> - ✅ 支持安全审核, 日志记录, 模型限速, API Gateway 等高级功能
> - ✅ 支持推广奖励,专业数据统计,用户画像分析等商业分析能力
> - ✅ 支持Discord/Telegram/飞书等机器人对接集成能力 (扩展模块)
> - ...
>
> [👉 了解更多](https://www.coai.dev/docs/contact)
## 🔨 支持模型
1. OpenAI & Azure OpenAI *(✅ Vision ✅ Function Calling)*
2. Anthropic Claude *(✅ Vision ✅ Function Calling)*
3. Google Gemini & PaLM2 *(✅ Vision)*
4. Midjourney *(✅ Mode Toggling ✅ U/V/R Actions)*
5. 讯飞星火 SparkDesk *(✅ Vision ✅ Function Calling)*
6. 智谱清言 ChatGLM *(✅ Vision)*
7. 通义千问 Tongyi Qwen
8. 腾讯混元 Tencent Hunyuan
9. 百川大模型 Baichuan AI
10. 月之暗面 Moonshot AI (👉 OpenAI)
11. 深度求索 DeepSeek AI (👉 OpenAI)
12. 字节云雀 ByteDance Skylark *(✅ Function Calling)*
13. Groq Cloud AI
14. OpenRouter (👉 OpenAI)
15. 360 GPT
16. LocalAI / Ollama (👉 OpenAI)
## 👻 中转 OpenAI 兼容 API
- [x] Chat Completions _(/v1/chat/completions)_
- [x] Image Generation _(/v1/images)_
- [x] Model List _(/v1/models)_
- [x] Dashboard Billing _(/v1/billing)_
## 📦 部署方式
> [!TIP]
> **部署成功后, 管理员账号为 `root`, 密码默认为 `chatnio123456`**
### ✨ Zeabur (一键部署)
[](https://zeabur.com/templates/M86XJI)
> Zeabur 提供一定的免费额度, 可以使用非付费区域进行一键部署,同时也支持计划订阅和弹性计费等方式弹性扩展。
> 1. 点击 `Deploy` 进行部署, 并输入你希望绑定的域名,等待部署完成。
> 2. 部署完成后, 请访问你的域名, 并使用用户名 `root` 密码 `chatnio123456` 登录后台管理,请按照提示在 chatnio 后台及时修改密码。
### 🐳 宝塔面板 (一键部署)
1. 安装宝塔面板,前往 [宝塔面板官网](https://www.bt.cn/new/download.html) 进行安装,选择正式版脚本安装。
2. 登录面板,点击左侧 **Docker** 进入 Docker 管理。
3. 如提示未安装 Docker / Docker Compose, 可根据上方引导安装。
4. 安装完成后,进入 **应用商城**,搜索 `CoAI` 并点击 **安装**。
5. 配置应用基本信息,如您的域名,端口等配置,并点击 **确认** (可使用默认配置)。
6. 首次安装可能需要等待 1-2 分钟完成数据库初始化。如遇到问题,请查看面板运行日志进行排查。
7. 访问您配置的域名或服务器 `http://[ip]:[port]`,使用用户名 `root` 和密码 `chatnio123456` 登录后台管理。
### 阿里云计算巢 (一键部署)
[](https://computenest.console.aliyun.com/service/instance/create/default?type=user&ServiceName=CoAI社区版)
1. 访问计算巢CoAI[部署链接](https://computenest.console.aliyun.com/service/instance/create/cn-hangzhou?type=user&ServiceName=CoAI社区版),按提示填写部署参数
2. 选择付费类型,填写实例参数与网络参数,点击 **确认订单**
3. 确认部署参数并查看预估价格后,点击立即创建,等待服务实例部署完成
4. 点击左侧 **服务实例** 等待服务实例部署完成后,点击实例ID进入到详情界面
5. 点击详情界面**立即使用**中的链接,可进入CoAI社区版界面。默认用户名为`root`,密码`为chatnio123456` 登录后台管理。
6. 更多操作详情与付费信息,参见:[服务详情](https://computenest.console.aliyun.com/service/detail/cn-hangzhou/service-bfbf676bd89d434691fc/1?type=user&isRecommend=true)
### ⚡ Docker Compose 安装 (推荐)
> [!NOTE]
> 运行成功后, 宿主机映射地址为 `http://localhost:8000`
```shell
git clone --depth=1 --branch=main --single-branch https://github.com/coaidev/coai.git
cd chatnio
docker-compose up -d # 运行服务
# 如需使用 stable 版本, 请使用 docker-compose -f docker-compose.stable.yaml up -d 替代
# 如需使用 watchtower 自动更新, 请使用 docker-compose -f docker-compose.watch.yaml up -d 替代
```
版本更新(_开启 Watchtower 自动更新的情况下, 无需手动更新_):
```shell
docker-compose down
docker-compose pull
docker-compose up -d
```
> - MySQL 数据库挂载目录项目 ~/**db**
> - Redis 数据库挂载目录项目 ~/**redis**
> - 配置文件挂载目录项目 ~/**config**
### ⚡ Docker 安装 (轻量运行时, 常用于外置 _MYSQL/RDS_ 服务)
> [!NOTE]
> 运行成功后, 宿主机地址为 `http://localhost:8094`。
>
> 如需使用 stable 版本, 请使用 `programzmh/chatnio:stable` 替代 `programzmh/chatnio:latest`
```shell
docker run -d --name chatnio \
--network host \
-v ~/config:/config \
-v ~/logs:/logs \
-v ~/storage:/storage \
-e MYSQL_HOST=localhost \
-e MYSQL_PORT=3306 \
-e MYSQL_DB=chatnio \
-e MYSQL_USER=root \
-e MYSQL_PASSWORD=chatnio123456 \
-e REDIS_HOST=localhost \
-e REDIS_PORT=6379 \
-e SECRET=secret \
-e SERVE_STATIC=true \
programzmh/chatnio:latest
```
> - *--network host* 指使用宿主机网络, 使 Docker 容器使用宿主机的网络, 可自行修改
> - SECRET: JWT 密钥, 自行生成随机字符串修改
> - SERVE_STATIC: 是否启用静态文件服务 (正常情况下不需要更改此项, 详见下方常见问题解答)
> - *-v ~/config:/config* 挂载配置文件, *-v ~/logs:/logs* 挂载日志文件的宿主机目录, *-v ~/storage:/storage* 挂载附加功能的生成文件
> - 需配置 MySQL 和 Redis 服务, 请自行参考上方信息修改环境变量
版本更新 (_开启 Watchtower 后无需手动更新, 执行后按照上述步骤重新运行即可_):
```shell
docker stop chatnio
docker rm chatnio
docker pull programzmh/chatnio:latest
```
### ⚒ 编译安装
> [!NOTE]
> 部署成功后, 默认端口为 **8094**, 访问地址为 `http://localhost:8094`
>
> Config 配置项 (~/config/**config.yaml**) 可以使用环境变量进行覆盖, 如 `MYSQL_HOST` 环境变量可覆盖 `mysql.host` 配置项
```shell
git clone https://github.com/coaidev/coai.git
cd chatnio
cd app
npm install -g pnpm
pnpm install
pnpm build
cd ..
go build -o chatnio
# e.g. using nohup (you can also use systemd or other service manager)
nohup ./chatnio > output.log & # using nohup to run in background
```
## ❓ 常见问题 Q&A
1. **为什么我部署后的站点可以访问页面, 可以登录注册, 但是无法使用聊天 (一直在转圈)?**
- 聊天等此类功能通过 websocket 进行通信, 请确保你的服务支持 websocket。 (Tip: 中转通过 Http 实现, 无需 websocket 支持)
- 如果你使用了 Nginx, Apache 等反向代理, 请确保已配置 websocket 支持。
- 如果使用了端口映射, 端口转发, CDN, API Gateway 等服务, 请确保你的服务支持并开启 websocket。
2. **我配置的 Midjourney Proxy 格式的渠道一直转圈或报错 `please provide available notify url`**
- 若为转圈,请确保你的 Midjourney Proxy 服务已正常运行, 并且已配置正确的上游地址。
- **Midjourney 要填渠道类型要用 Midjourney 而不是 OpenAI (不知道为什么很多人填成了 OpenAI 类型格式然后过来反馈为什么empty response, mj-chat 类除外)**
- 排查完这些问题后, 请查看你的系统设置中的**后端域名**是否已经配置并配置正确。如果不配置, 将导致 Midjourney Proxy 服务无法正常回调。
3. **此项目有什么外部依赖?**
- MySQL: 存储用户信息, 对话记录, 管理员信息等持久化数据。
- Redis: 存储用户快速鉴权信息, IP 速率限制, 订阅配额, 邮箱验证码等数据。
- 环境未配置好的情况下, 会导致服务无法正常运行, 请确保你的 MySQL 和 Redis 服务已正常运行 (Docker 部署, 编译部署需自行搭建外部服务)。
4. **我的机器为 ARM 架构, 该项目支持 ARM 架构吗?**
- 支持。CoAI.Dev 项目使用 BuildX 构建多架构镜像, 你可以直接使用 docker-compose 或 docker 运行, 无需额外配置。
- 如果你使用编译安装, 直接在 ARM 机器上编译即可, 无需欸外配置。如果你使用 x86 机器编译, 请使用 `GOARCH=arm64 go build -o chatnio` 进行交叉编译并上传至 ARM 机器上运行。
5. **如何修改 Root 默认密码?**
- 请点击右上角头像或侧边栏底部用户框进入后台管理, 点击系统设置下常规设置操作栏的 修改 Root 密码 进行修改。或者选择在 用户管理 中选定 root 用户进行修改密码操作。
6. **系统设置中的后端域名是什么?**
- 后端域名是指后端 API 服务的地址, 默认为你访问站点后加 `/api` 的地址, 如 `https://example.com/api` 。
- 如果设置为非 *SERVE_STATIC* 模式, 开启前后端分离部署, 请将后端域名设置为你的后端 API 服务地址, 如 `https://api.example.com`。
- 后端域名此处用于 Midjourney Proxy 服务的后端回调地址, 如无需使用 Midjourney Proxy 服务, 请忽略此设置。
7. **如何配置支付方式?**
- CoAI.Dev 开源版支持发卡模式, 设置系统设置中的购买链接为你的发卡地址即可。卡密可通过用户管理中兑换码管理中批量生成。
8. **礼品码和兑换码有什么区别?**
- 礼品码一种类型只能一个用户只能绑定一次, 而非 aff code, 发福利等方式可使用礼品码, 可在头像下拉菜单中的礼品码中兑换。
- 兑换码一种类型可以多个用户绑定, 可作为正常购买和发卡使用, 可在用户管理中的兑换码管理中批量生成, 在头像下拉菜单的点数(菜单第一个)内输入兑换码进行兑换。
- 一个例子:比如我发了一个类型为 *新年快乐* 的福利, 此时推荐使用礼品码, 假设发放 100 个 66 点数, 如果为兑换码, 手快的一个用户就批量把所有兑换码的 6600 点数都用完了, 而礼品码则可以保证每个用户只能使用一次 (获得 66 点数)。
- 而搭建发卡的时, 如果用礼品码, 因为一个类型只能兑换一次, 购买多个礼品码会导致兑换失败, 而兑换码则可以在此场景下使用。
9. **该项目支持 Vercel 部署吗?**
- CoAI.Dev 本身并不支持 Vercel 部署, 但是你可以使用前后端分离模式, Vercel 部署前端部分, 后端部分使用 Docker 部署或编译部署。
10. **前后端分离部署模式是什么?**
- 正常情况下, 前后端在同一服务内, 后端地址为 `/api`。前后端分离部署指前端和后端分别部署在不同的服务上, 前端服务为静态文件服务, 后端服务为 API 服务。
- 举个例子, 前端使用 Nginx (或 Vercel 等) 部署, 部署的域名为 `https://www.chatnio.net`。
- 后端使用 Docker 部署, 部署的域名为 `https://api.chatnio.net`。
- 此种部署方式需自行打包前端, 配置环境变量 `VITE_BACKEND_ENDPOINT` 为你的后端地址, 如 `https://api.chatnio.net`。
- 配置后端环境变量的 `SERVE_STATIC=false` 使后端服务不提供静态文件服务。
11. **弹性计费和订阅详解**
- 弹性计费, 即 `点数`, 其图标类似于**云**, 模型计费通用方式, 为了防止虚假汇率, 写死 10 点数 = 1 元, 汇率可以在计费规则中的 **应用内置模板** 中自定义汇率。
- 订阅, 即订阅计划, 为固定价格计费方式按次配额, 订阅计费扣取点数 (举例: 如果站点的用户想订阅 32 元的计划, 则需要保证点数大于等于 320 点数)
- 订阅是 Item 的组合, 每个 Item 都可设置涵盖的模型, 订阅配额 (-1 为无限使用), 名称, ID (用于区分不同的 Item), 图标等。可在后台的订阅管理中进行操作, 是否开启订阅, 订阅价格等, 修改每个订阅等级的 Item, 以及支持直接导入其他订阅等级的 Item。
- 订阅支持分层并写死为三个等级。 等级分别为: _普通用户 (0)_, _基础版订阅 (1)_, _标准版订阅 (2)_, _专业版订阅 (3)_, 订阅等级即为用户分组, 可在渠道管理中进行高级设置, 选择勾选可使用此模型的用户分组。
- 订阅配额设置, 可在订阅管理中进行操作, 是否支持中转 API (默认关闭)
12. **可请求最小点数检测 `user quota is not enough` 详解**
- 为防止站点用户滥用站点模型, 当请求点数低于最小请求点数时将返回点数不足的错误信息, 大于等于最小请求点数时将正常请求。
- 模型的最小可请求点数规则:
- 不计费模型无限制
- 次数计费模型最小点数为该模型的 1 次请求点数 (e.g. 若一个模型的单次请求点数为 0.1 点数, 则最小请求点数为 0.1 点数)
- Token 弹性计费模型为 1K 输入 Tokens 价格 + 1K 输出 Tokens 价格 (e.g. 若一个模型的 1K 输入 Tokens 价格为 0.05 点数, 1K 输出 Tokens 价格 0.1 点数, 则最小请求点数为 0.15 点数)
13. **为何我的 GPT-4-All 等逆向模型无法使用上传文件中的图片?**
- 上传模型图片为 Base64 格式, 如果逆向不支持 Base64 格式, 请使用 URL 直链而非上传文件做法。
14. **如何开始域名严格跨域检测?**
- 正常情况下,后端对所有域名开放跨域。如果非特殊需求,无需开启严格跨域检测。
- 如果需要开启严格跨域检测,可以在后端环境变量中 并配置 `ALLOW_ORIGINS`, 如 `ALLOW_ORIGINS=chatnio.net,chatnio.app` (不需要加协议前缀, www 解析无需手动添加, 后端将自动识别并允许跨域), 这样就会支持严格跨域检测 (如 *http://www.chatnio.app*, *https://chatnio.net* 等将会被允许, 其他域名将会被拒绝)。
- 即使在开启严格跨域检测的情况下, /v1 接口会被仍然允许所有域的跨域请求, 以保证中转 API 的正常使用。
15. **模型映射功能是如何使用的?**
- 渠道内的模型映射格式为 `[from]>[to]`, 多个映射之间换行, **from** 为请求的模型, **to** 为真实向上游发送的模型并且需要上游真实支持
- 如: 我有一个逆向渠道, 填写 `gpt-4-all>gpt-4`, 则我的用户请求 **gpt-4-all** 模型到该渠道时, 后端则会模型映射至 **gpt-4** 向该渠道请求 **gpt-4**, 此时该渠道支持 2 个模型, **gpt-4** 和 **gpt-4-all** (本质上都为 **gpt-4**)
- 如果我不想让我的这个逆向渠道影响到 **gpt-4** 的渠道组, 可以加前缀 `!gpt-4-all>gpt-4`, 该渠道 **gpt-4** 则会被忽略, 此时该渠道将只支持 1 个模型, **gpt-4-all** (但本质上为 **gpt-4**)
## 📦 技术栈
- 🥗 前端: React + Redux + Radix UI + Tailwind CSS
- 🍎 后端: Golang + Gin + Redis + MySQL
- 🍒 应用技术: PWA + WebSocket
## 🤯 为什么写此项目 & 项目优势
我们发现,市面上的 AIGC 商业站点,大多数都是偏向于前端轻量部署的项目,有精美的 UI 界面设计,
比如 [Next Chat](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) 的二开商业版本,
由于其偏向个人私有化的设计,在二开商业化时有一定的局限性,呈现出一些问题,比如:
- **对话同步难**, 比如需要 WebDav 等服务,用户学习成本高,跨端实时同步困难。
- **计费不够完善**, 比如只支持弹性计费或只支持订阅制,无法满足不同用户的需求。
- **文件解析不便捷**, 比如只支持先在图床上传图片,返回站点后再在输入框中输入 URL 直链,无内置文件解析功能。
- **不支持对话 URL 分享**, 比如只支持对话截图分享,无法支持对话 URL 分享 (或仅支持 ShareGPT 等工具,无法对站点起到推广作用)。
- **渠道管理不够强大**, 比如后台仅支持 OpenAI 格式渠道,兼容其他格式渠道困难。且只能填入一个渠道,无法支持多渠道管理。
- **不支持 API 调用**, 比如只支持用户界面调用,无法支持 API 中转和管理。
另一种是偏向于 API 分发的站点,有强大的分发系统,比如基于 [One API](https://github.com/songquanpeng/one-api) 等项目,
这类项目虽然支持强大的 API 中转和管理,但是缺少界面设计,且缺少一些 C 端功能,比如:
- **用户界面不够丰富**, 比如只支持 API 调用,不内置用户界面聊天。用户界面聊天需要自行复制密钥并前往其他站点才能使用,这对于普通用户来说,学习成本较高。
- **没有订阅制**, 比如只支持弹性计费,缺少对 C 端用户的计费设计,无法满足用户的不同需求,对于无基础的用户来说,成本感知不够友好。
- **C 端功能不够丰富**, 比如只支持 API 调用,不支持对话同步,不支持对话分享,不支持文件解析等功能。
- **均衡负载不够强大**, 开源版不支持**权重**参数, 无法实现同优先级的渠道均衡负载分配概率 ([New API](https://github.com/Calcium-Ion/new-api) 也解决了此痛点, UI 也更美观)。
因此,我们希望能够将这两种项目的优势结合起来,做出一个既有强大的 API 分发系统,又有丰富的用户界面设计的项目,
这样既能满足 C 端用户的需求,又能发展 B 端业务,提高用户体验,降低用户学习成本,提高用户粘性。
于是,**CoAI.Dev** 应运而生,我们希望能够做出一个既有强大的 API 分发系统,又有丰富的用户界面设计的项目,成为下一代开源 AIGC 项目的商业一站式解决方案。
## ❤ 捐助
如果您觉得这个项目对您有所帮助, 您可以点个 Star 支持一下!
================================================
FILE: adapter/adapter.go
================================================
package adapter
import (
"chat/adapter/azure"
"chat/adapter/baichuan"
"chat/adapter/bing"
"chat/adapter/claude"
adaptercommon "chat/adapter/common"
"chat/adapter/coze"
"chat/adapter/dashscope"
"chat/adapter/deepseek"
"chat/adapter/dify"
"chat/adapter/hunyuan"
"chat/adapter/midjourney"
"chat/adapter/openai"
"chat/adapter/palm2"
"chat/adapter/skylark"
"chat/adapter/slack"
"chat/adapter/sparkdesk"
"chat/adapter/zhinao"
"chat/adapter/zhipuai"
"chat/globals"
"fmt"
)
var channelFactories = map[string]adaptercommon.FactoryCreator{
globals.OpenAIChannelType: openai.NewChatInstanceFromConfig,
globals.AzureOpenAIChannelType: azure.NewChatInstanceFromConfig,
globals.ClaudeChannelType: claude.NewChatInstanceFromConfig,
globals.SlackChannelType: slack.NewChatInstanceFromConfig,
globals.BingChannelType: bing.NewChatInstanceFromConfig,
globals.PalmChannelType: palm2.NewChatInstanceFromConfig,
globals.SparkdeskChannelType: sparkdesk.NewChatInstanceFromConfig,
globals.ChatGLMChannelType: zhipuai.NewChatInstanceFromConfig,
globals.QwenChannelType: dashscope.NewChatInstanceFromConfig,
globals.HunyuanChannelType: hunyuan.NewChatInstanceFromConfig,
globals.BaichuanChannelType: baichuan.NewChatInstanceFromConfig,
globals.SkylarkChannelType: skylark.NewChatInstanceFromConfig,
globals.ZhinaoChannelType: zhinao.NewChatInstanceFromConfig,
globals.MidjourneyChannelType: midjourney.NewChatInstanceFromConfig,
globals.DeepseekChannelType: deepseek.NewChatInstanceFromConfig,
globals.DifyChannelType: dify.NewChatInstanceFromConfig,
globals.CozeChannelType: coze.NewChatInstanceFromConfig,
globals.MoonshotChannelType: openai.NewChatInstanceFromConfig, // openai format
globals.GroqChannelType: openai.NewChatInstanceFromConfig, // openai format
}
func createChatRequest(conf globals.ChannelConfig, props *adaptercommon.ChatProps, hook globals.Hook) error {
props.Model = conf.GetModelReflect(props.OriginalModel)
props.Proxy = conf.GetProxy()
factoryType := conf.GetType()
if factory, ok := channelFactories[factoryType]; ok {
return factory(conf).CreateStreamChatRequest(props, hook)
}
return fmt.Errorf("unknown channel type %s (channel #%d)", conf.GetType(), conf.GetId())
}
func createVideoRequest(conf globals.ChannelConfig, props *adaptercommon.VideoProps, hook globals.Hook) error {
props.Model = conf.GetModelReflect(props.OriginalModel)
props.Proxy = conf.GetProxy()
factoryType := conf.GetType()
if creator, ok := channelFactories[factoryType]; ok {
inst := creator(conf)
if v, ok := inst.(adaptercommon.VideoFactory); ok {
return v.CreateVideoRequest(props, hook)
}
return fmt.Errorf("video request not supported by channel type %s (channel #%d)", conf.GetType(), conf.GetId())
}
return fmt.Errorf("unknown channel type %s (channel #%d)", conf.GetType(), conf.GetId())
}
================================================
FILE: adapter/azure/chat.go
================================================
package azure
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"errors"
"fmt"
"strings"
)
func (c *ChatInstance) GetChatEndpoint(props *adaptercommon.ChatProps) string {
model := strings.ReplaceAll(props.Model, ".", "")
if props.Model == globals.GPT3TurboInstruct {
return fmt.Sprintf("%s/openai/deployments/%s/completions?api-version=%s", c.GetResource(), model, c.GetEndpoint())
}
return fmt.Sprintf("%s/openai/deployments/%s/chat/completions?api-version=%s", c.GetResource(), model, c.GetEndpoint())
}
func (c *ChatInstance) GetCompletionPrompt(messages []globals.Message) string {
result := ""
for _, message := range messages {
result += fmt.Sprintf("%s: %s\n", message.Role, message.Content)
}
return result
}
func (c *ChatInstance) GetLatestPrompt(props *adaptercommon.ChatProps) string {
if len(props.Message) == 0 {
return ""
}
return props.Message[len(props.Message)-1].Content
}
func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} {
if props.Model == globals.GPT3TurboInstruct {
// for completions
return CompletionRequest{
Prompt: c.GetCompletionPrompt(props.Message),
MaxToken: props.MaxTokens,
Stream: stream,
}
}
return ChatRequest{
Messages: formatMessages(props),
MaxToken: props.MaxTokens,
Stream: stream,
PresencePenalty: props.PresencePenalty,
FrequencyPenalty: props.FrequencyPenalty,
Temperature: props.Temperature,
TopP: props.TopP,
Tools: props.Tools,
ToolChoice: props.ToolChoice,
}
}
// CreateChatRequest is the native http request body for openai
func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {
if globals.IsOpenAIDalleModel(props.Model) {
return c.CreateImage(props)
}
res, err := utils.Post(
c.GetChatEndpoint(props),
c.GetHeader(),
c.GetChatBody(props, false),
props.Proxy,
)
if err != nil || res == nil {
return "", fmt.Errorf("openai error: %s", err.Error())
}
data := utils.MapToStruct[ChatResponse](res)
if data == nil {
return "", fmt.Errorf("openai error: cannot parse response")
} else if data.Error.Message != "" {
return "", fmt.Errorf("openai error: %s", data.Error.Message)
}
return data.Choices[0].Message.Content, nil
}
// CreateStreamChatRequest is the stream response body for openai
func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {
if globals.IsOpenAIDalleModel(props.Model) {
if url, err := c.CreateImage(props); err != nil {
return err
} else {
return callback(&globals.Chunk{Content: url})
}
}
isCompletionType := props.Model == globals.GPT3TurboInstruct
ticks := 0
err := utils.EventScanner(&utils.EventScannerProps{
Method: "POST",
Uri: c.GetChatEndpoint(props),
Headers: c.GetHeader(),
Body: c.GetChatBody(props, true),
Callback: func(data string) error {
ticks += 1
partial, err := c.ProcessLine(data, isCompletionType)
if err != nil {
return err
}
return callback(partial)
},
}, props.Proxy)
if err != nil {
if form := processChatErrorResponse(err.Body); form != nil {
msg := fmt.Sprintf("%s (type: %s)", form.Error.Message, form.Error.Type)
return errors.New(msg)
}
return err.Error
}
if ticks == 0 {
return errors.New("no response")
}
return nil
}
================================================
FILE: adapter/azure/image.go
================================================
package azure
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"fmt"
"strings"
)
type ImageProps struct {
Model string
Prompt string
Size ImageSize
Proxy globals.ProxyConfig
}
func (c *ChatInstance) GetImageEndpoint(model string) string {
model = strings.ReplaceAll(model, ".", "")
return fmt.Sprintf("%s/openai/deployments/%s/images/generations?api-version=%s", c.GetResource(), model, c.GetEndpoint())
}
// CreateImageRequest will create a dalle image from prompt, return url of image, base64 data and error
func (c *ChatInstance) CreateImageRequest(props ImageProps) (string, string, error) {
res, err := utils.Post(
c.GetImageEndpoint(props.Model),
c.GetHeader(), ImageRequest{
Prompt: props.Prompt,
Size: utils.Multi[ImageSize](
props.Model == globals.Dalle3 || props.Model == globals.GPTImage1,
ImageSize1024,
ImageSize512,
),
N: 1,
}, props.Proxy)
if err != nil || res == nil {
return "", "", fmt.Errorf("openai error: %s", err.Error())
}
data := utils.MapToStruct[ImageResponse](res)
if data == nil {
return "", "", fmt.Errorf("openai error: cannot parse response")
} else if data.Error.Message != "" {
return "", "", fmt.Errorf("openai error: %s", data.Error.Message)
}
// for gpt-image-1, return base64 data if available
if props.Model == globals.GPTImage1 && data.Data[0].B64Json != "" {
return "", data.Data[0].B64Json, nil
}
return data.Data[0].Url, "", nil
}
// CreateImage will create a dalle image from prompt, return markdown of image
func (c *ChatInstance) CreateImage(props *adaptercommon.ChatProps) (string, error) {
url, b64Json, err := c.CreateImageRequest(ImageProps{
Model: props.Model,
Prompt: c.GetLatestPrompt(props),
Proxy: props.Proxy,
})
if err != nil {
if strings.Contains(err.Error(), "safety") {
return err.Error(), nil
}
return "", err
}
if b64Json != "" {
return utils.GetBase64ImageMarkdown(b64Json), nil
}
return utils.GetImageMarkdown(url), nil
}
================================================
FILE: adapter/azure/processor.go
================================================
package azure
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"errors"
"fmt"
"regexp"
)
func formatMessages(props *adaptercommon.ChatProps) interface{} {
if globals.IsVisionModel(props.Model) {
return utils.Each[globals.Message, Message](props.Message, func(message globals.Message) Message {
if message.Role == globals.User {
raw, urls := utils.ExtractImages(message.Content, true)
images := utils.EachNotNil[string, MessageContent](urls, func(url string) *MessageContent {
obj, err := utils.NewImage(url)
if err != nil {
globals.Info(fmt.Sprintf("cannot process image: %s (source: %s)", err.Error(), utils.Extract(url, 24, "...")))
return nil
}
props.Buffer.AddImage(obj)
return &MessageContent{
Type: "image_url",
ImageUrl: &ImageUrl{
Url: url,
},
}
})
return Message{
Role: message.Role,
Content: utils.Prepend(images, MessageContent{
Type: "text",
Text: &raw,
}),
Name: message.Name,
FunctionCall: message.FunctionCall,
ToolCalls: message.ToolCalls,
ToolCallId: message.ToolCallId,
}
}
return Message{
Role: message.Role,
Content: MessageContents{
MessageContent{
Type: "text",
Text: &message.Content,
},
},
Name: message.Name,
FunctionCall: message.FunctionCall,
ToolCalls: message.ToolCalls,
ToolCallId: message.ToolCallId,
}
})
}
return props.Message
}
func processChatResponse(data string) *ChatStreamResponse {
return utils.UnmarshalForm[ChatStreamResponse](data)
}
func processCompletionResponse(data string) *CompletionResponse {
return utils.UnmarshalForm[CompletionResponse](data)
}
func processChatErrorResponse(data string) *ChatStreamErrorResponse {
return utils.UnmarshalForm[ChatStreamErrorResponse](data)
}
func getChoices(form *ChatStreamResponse) *globals.Chunk {
if len(form.Choices) == 0 {
return &globals.Chunk{Content: ""}
}
choice := form.Choices[0].Delta
return &globals.Chunk{
Content: choice.Content,
ToolCall: choice.ToolCalls,
FunctionCall: choice.FunctionCall,
}
}
func getCompletionChoices(form *CompletionResponse) string {
if len(form.Choices) == 0 {
return ""
}
return form.Choices[0].Text
}
func getRobustnessResult(chunk string) string {
exp := `\"content\":\"(.*?)\"`
compile, err := regexp.Compile(exp)
if err != nil {
return ""
}
matches := compile.FindStringSubmatch(chunk)
if len(matches) > 1 {
return utils.ProcessRobustnessChar(matches[1])
} else {
return ""
}
}
func (c *ChatInstance) ProcessLine(data string, isCompletionType bool) (*globals.Chunk, error) {
if isCompletionType {
// openai legacy support
if completion := processCompletionResponse(data); completion != nil {
return &globals.Chunk{
Content: getCompletionChoices(completion),
}, nil
}
globals.Warn(fmt.Sprintf("openai error: cannot parse completion response: %s", data))
return &globals.Chunk{Content: ""}, errors.New("parser error: cannot parse completion response")
}
if form := processChatResponse(data); form != nil {
return getChoices(form), nil
}
if form := processChatErrorResponse(data); form != nil {
return &globals.Chunk{Content: ""}, errors.New(fmt.Sprintf("openai error: %s (type: %s)", form.Error.Message, form.Error.Type))
}
globals.Warn(fmt.Sprintf("openai error: cannot parse chat completion response: %s", data))
return &globals.Chunk{Content: ""}, errors.New("parser error: cannot parse chat completion response")
}
================================================
FILE: adapter/azure/struct.go
================================================
package azure
import (
factory "chat/adapter/common"
"chat/globals"
)
type ChatInstance struct {
Endpoint string
ApiKey string
Resource string
}
func (c *ChatInstance) GetEndpoint() string {
return c.Endpoint
}
func (c *ChatInstance) GetApiKey() string {
return c.ApiKey
}
func (c *ChatInstance) GetResource() string {
return c.Resource
}
func (c *ChatInstance) GetHeader() map[string]string {
return map[string]string{
"Content-Type": "application/json",
"api-key": c.GetApiKey(),
}
}
func NewChatInstance(endpoint, apiKey string, resource string) *ChatInstance {
return &ChatInstance{
Endpoint: endpoint,
ApiKey: apiKey,
Resource: resource,
}
}
func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {
param := conf.SplitRandomSecret(2)
return NewChatInstance(
conf.GetEndpoint(),
param[0],
param[1],
)
}
================================================
FILE: adapter/azure/types.go
================================================
package azure
import "chat/globals"
type ImageUrl struct {
Url string `json:"url"`
Detail *string `json:"detail,omitempty"`
}
type MessageContent struct {
Type string `json:"type"`
Text *string `json:"text,omitempty"`
ImageUrl *ImageUrl `json:"image_url,omitempty"`
}
type MessageContents []MessageContent
type Message struct {
Role string `json:"role"`
Content MessageContents `json:"content"`
Name *string `json:"name,omitempty"`
FunctionCall *globals.FunctionCall `json:"function_call,omitempty"` // only `function` role
ToolCallId *string `json:"tool_call_id,omitempty"` // only `tool` role
ToolCalls *globals.ToolCalls `json:"tool_calls,omitempty"` // only `assistant` role
}
// ChatRequest is the request body for openai
type ChatRequest struct {
Model string `json:"model"`
Messages interface{} `json:"messages"`
MaxToken *int `json:"max_tokens,omitempty"`
MaxCompletionTokens *int `json:"max_completion_tokens,omitempty"`
Stream bool `json:"stream"`
PresencePenalty *float32 `json:"presence_penalty,omitempty"`
FrequencyPenalty *float32 `json:"frequency_penalty,omitempty"`
Temperature *float32 `json:"temperature,omitempty"`
TopP *float32 `json:"top_p,omitempty"`
Tools *globals.FunctionTools `json:"tools,omitempty"`
ToolChoice *interface{} `json:"tool_choice,omitempty"` // string or object
}
// CompletionRequest is the request body for openai completion
type CompletionRequest struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
MaxToken *int `json:"max_tokens,omitempty"`
Stream bool `json:"stream"`
}
// ChatResponse is the native http request body for openai
type ChatResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []struct {
Index int `json:"index"`
Message globals.Message `json:"message"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
Error struct {
Message string `json:"message"`
} `json:"error"`
}
// ChatStreamResponse is the stream response body for openai
type ChatStreamResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []struct {
Delta globals.Message `json:"delta"`
Index int `json:"index"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
}
// CompletionResponse is the native http request body / stream response body for openai completion
type CompletionResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []struct {
Text string `json:"text"`
Index int `json:"index"`
} `json:"choices"`
}
type ChatStreamErrorResponse struct {
Error struct {
Message string `json:"message"`
Type string `json:"type"`
} `json:"error"`
}
type ImageSize string
// ImageRequest is the request body for openai dalle image generation
type ImageRequest struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
Size ImageSize `json:"size"`
N int `json:"n"`
}
type ImageResponse struct {
Data []struct {
Url string `json:"url,omitempty"`
B64Json string `json:"b64_json,omitempty"`
} `json:"data"`
Error struct {
Message string `json:"message"`
} `json:"error"`
Usage *struct {
InputTokens int `json:"input_tokens"`
InputTokensDetails struct {
ImageTokens int `json:"image_tokens"`
TextTokens int `json:"text_tokens"`
} `json:"input_tokens_details"`
OutputTokens int `json:"output_tokens"`
TotalTokens int `json:"total_tokens"`
} `json:"usage,omitempty"`
}
var (
ImageSize256 ImageSize = "256x256"
ImageSize512 ImageSize = "512x512"
ImageSize1024 ImageSize = "1024x1024"
)
================================================
FILE: adapter/baichuan/chat.go
================================================
package baichuan
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"errors"
"fmt"
)
func (c *ChatInstance) GetChatEndpoint() string {
return fmt.Sprintf("%s/v1/chat/completions", c.GetEndpoint())
}
func (c *ChatInstance) GetModel(model string) string {
switch model {
case globals.Baichuan53B:
return "Baichuan2"
default:
return model
}
}
func (c *ChatInstance) GetMessages(messages []globals.Message) []globals.Message {
for _, message := range messages {
if message.Role == globals.System || message.Role == globals.Tool {
message.Role = globals.User
}
}
return messages
}
func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) ChatRequest {
return ChatRequest{
Model: c.GetModel(props.Model),
Messages: c.GetMessages(props.Message),
Stream: stream,
TopP: props.TopP,
TopK: props.TopK,
Temperature: props.Temperature,
}
}
// CreateChatRequest is the native http request body for baichuan
func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {
res, err := utils.Post(
c.GetChatEndpoint(),
c.GetHeader(),
c.GetChatBody(props, false),
props.Proxy,
)
if err != nil || res == nil {
return "", fmt.Errorf("baichuan error: %s", err.Error())
}
data := utils.MapToStruct[ChatResponse](res)
if data == nil {
return "", fmt.Errorf("baichuan error: cannot parse response")
} else if data.Error.Message != "" {
return "", fmt.Errorf("baichuan error: %s", data.Error.Message)
}
return data.Choices[0].Message.Content, nil
}
// CreateStreamChatRequest is the stream response body for baichuan
func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {
err := utils.EventScanner(&utils.EventScannerProps{
Method: "POST",
Uri: c.GetChatEndpoint(),
Headers: c.GetHeader(),
Body: c.GetChatBody(props, true),
Callback: func(data string) error {
partial, err := c.ProcessLine(data)
if err != nil {
return err
}
return callback(partial)
},
}, props.Proxy)
if err != nil {
if form := processChatErrorResponse(err.Body); form != nil {
msg := fmt.Sprintf("%s (type: %s)", form.Error.Message, form.Error.Type)
return errors.New(msg)
}
return err.Error
}
return nil
}
================================================
FILE: adapter/baichuan/processor.go
================================================
package baichuan
import (
"chat/globals"
"chat/utils"
"errors"
"fmt"
)
func processChatResponse(data string) *ChatStreamResponse {
return utils.UnmarshalForm[ChatStreamResponse](data)
}
func processChatErrorResponse(data string) *ChatStreamErrorResponse {
return utils.UnmarshalForm[ChatStreamErrorResponse](data)
}
func getChoices(form *ChatStreamResponse) *globals.Chunk {
if len(form.Choices) == 0 {
return &globals.Chunk{Content: ""}
}
choice := form.Choices[0].Delta
return &globals.Chunk{
Content: choice.Content,
}
}
func (c *ChatInstance) ProcessLine(data string) (*globals.Chunk, error) {
if form := processChatResponse(data); form != nil {
return getChoices(form), nil
}
if form := processChatErrorResponse(data); form != nil {
return &globals.Chunk{Content: ""}, errors.New(fmt.Sprintf("baichuan error: %s (type: %s)", form.Error.Message, form.Error.Type))
}
globals.Warn(fmt.Sprintf("baichuan error: cannot parse chat completion response: %s", data))
return &globals.Chunk{Content: ""}, errors.New("parser error: cannot parse chat completion response")
}
================================================
FILE: adapter/baichuan/struct.go
================================================
package baichuan
import (
factory "chat/adapter/common"
"chat/globals"
"fmt"
)
type ChatInstance struct {
Endpoint string
ApiKey string
}
func (c *ChatInstance) GetEndpoint() string {
return c.Endpoint
}
func (c *ChatInstance) GetApiKey() string {
return c.ApiKey
}
func (c *ChatInstance) GetHeader() map[string]string {
return map[string]string{
"Content-Type": "application/json",
"Authorization": fmt.Sprintf("Bearer %s", c.GetApiKey()),
}
}
func NewChatInstance(endpoint, apiKey string) *ChatInstance {
return &ChatInstance{
Endpoint: endpoint,
ApiKey: apiKey,
}
}
func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {
return NewChatInstance(
conf.GetEndpoint(),
conf.GetRandomSecret(),
)
}
================================================
FILE: adapter/baichuan/types.go
================================================
package baichuan
import "chat/globals"
// Baichuan AI API is similar to OpenAI API
type ChatRequest struct {
Model string `json:"model"`
Messages []globals.Message `json:"messages"`
Stream bool `json:"stream"`
TopP *float32 `json:"top_p,omitempty"`
TopK *int `json:"top_k,omitempty"`
Temperature *float32 `json:"temperature,omitempty"`
WithSearchEnhance *bool `json:"with_search_enhance,omitempty"`
}
// ChatResponse is the native http request body for baichuan
type ChatResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []struct {
Message struct {
Content string `json:"content"`
}
} `json:"choices"`
Error struct {
Message string `json:"message"`
} `json:"error"`
}
// ChatStreamResponse is the stream response body for baichuan
type ChatStreamResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []struct {
Delta struct {
Content string `json:"content"`
}
Index int `json:"index"`
} `json:"choices"`
}
type ChatStreamErrorResponse struct {
Error struct {
Message string `json:"message"`
Type string `json:"type"`
} `json:"error"`
}
================================================
FILE: adapter/bing/chat.go
================================================
package bing
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"fmt"
"strings"
)
func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, hook globals.Hook) error {
var conn *utils.WebSocket
if conn = utils.NewWebsocketClient(c.GetEndpoint()); conn == nil {
return fmt.Errorf("bing error: websocket connection failed")
}
defer conn.DeferClose()
model := strings.TrimPrefix(props.Model, "bing-")
prompt := props.Message[len(props.Message)-1].Content
if err := conn.SendJSON(&ChatRequest{
Prompt: prompt,
Hash: c.Secret,
Model: model,
}); err != nil {
return err
}
for {
form, err := utils.ReadForm[ChatResponse](conn)
if err != nil {
if strings.Contains(err.Error(), "websocket: close 1000") {
return nil
}
globals.Debug(fmt.Sprintf("bing error: %s", err.Error()))
return nil
}
if err := hook(&globals.Chunk{
Content: form.Response,
}); err != nil {
return err
}
}
}
================================================
FILE: adapter/bing/struct.go
================================================
package bing
import (
factory "chat/adapter/common"
"chat/globals"
"fmt"
)
type ChatInstance struct {
Endpoint string
Secret string
}
func (c *ChatInstance) GetEndpoint() string {
return fmt.Sprintf("%s/chat", c.Endpoint)
}
func NewChatInstance(endpoint, secret string) *ChatInstance {
return &ChatInstance{
Endpoint: endpoint,
Secret: secret,
}
}
func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {
return NewChatInstance(
conf.GetEndpoint(),
conf.GetRandomSecret(),
)
}
================================================
FILE: adapter/bing/types.go
================================================
package bing
// see https://github.com/Deeptrain-Community/chatnio-bing-service
type ChatRequest struct {
Prompt string `json:"prompt"`
Hash string `json:"hash"`
Model string `json:"model"`
}
type ChatResponse struct {
Response string `json:"response"`
}
================================================
FILE: adapter/claude/chat.go
================================================
package claude
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"errors"
"fmt"
)
const defaultTokens = 2500
func (c *ChatInstance) GetChatEndpoint() string {
return fmt.Sprintf("%s/v1/messages", c.GetEndpoint())
}
func (c *ChatInstance) GetChatHeaders() map[string]string {
return map[string]string{
"content-type": "application/json",
"anthropic-version": "2023-06-01",
"x-api-key": c.GetApiKey(),
}
}
// ConvertCompletionMessage converts the completion message to anthropic complete format (deprecated)
func (c *ChatInstance) ConvertCompletionMessage(message []globals.Message) string {
mapper := map[string]string{
globals.System: "Assistant",
globals.User: "Human",
globals.Assistant: "Assistant",
}
var result string
for i, item := range message {
if item.Role == globals.Tool {
continue
}
if i == 0 && item.Role == globals.Assistant {
// skip first assistant message
continue
}
result += fmt.Sprintf("\n\n%s: %s", mapper[item.Role], item.Content)
}
return fmt.Sprintf("%s\n\nAssistant:", result)
}
func (c *ChatInstance) GetTokens(props *adaptercommon.ChatProps) int {
if props.MaxTokens == nil || *props.MaxTokens <= 0 {
return defaultTokens
}
return *props.MaxTokens
}
func (c *ChatInstance) ConvertMessages(props *adaptercommon.ChatProps) []globals.Message {
// anthropic api: top message must be user message, only `user` and `assistant` role messages are allowd
start := false
result := make([]globals.Message, 0)
for _, message := range props.Message {
if message.Role == globals.System {
continue
}
// if is first message, set it to user message
if !start {
start = true
result = append(result, globals.Message{
Role: globals.User,
Content: message.Content,
})
continue
}
// anthropic api does not allow multi-same role messages
if len(result) > 0 && result[len(result)-1].Role == message.Role {
result[len(result)-1].Content += "\n" + message.Content
continue
}
result = append(result, message)
}
return result
}
func (c *ChatInstance) GetMessages(props *adaptercommon.ChatProps) []Message {
converted := c.ConvertMessages(props)
return utils.Each(converted, func(message globals.Message) Message {
if !globals.IsVisionModel(props.Model) || message.Role != globals.User {
return Message{
Role: message.Role,
Content: message.Content,
}
}
content, urls := utils.ExtractImages(message.Content, true)
images := utils.EachNotNil(urls, func(url string) *MessageContent {
obj, err := utils.NewImage(url)
props.Buffer.AddImage(obj)
if err != nil {
globals.Info(fmt.Sprintf("cannot process image: %s (source: %s)", err.Error(), utils.Extract(url, 24, "...")))
}
i := utils.NewImageContent(url)
return &MessageContent{
Type: "image",
Source: &MessageImage{
Type: "base64",
MediaType: i.GetType(),
Data: i.ToRawBase64(),
},
}
})
return Message{
Role: message.Role,
Content: utils.Prepend(images, MessageContent{
Type: "text",
Text: &content,
}),
}
})
}
func (c *ChatInstance) GetSystemPrompt(props *adaptercommon.ChatProps) (prompt string) {
for _, message := range props.Message {
if message.Role == globals.System {
prompt += message.Content
}
}
return
}
func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) *ChatBody {
messages := c.GetMessages(props)
return &ChatBody{
Messages: messages,
MaxTokens: c.GetTokens(props),
Model: props.Model,
System: c.GetSystemPrompt(props),
Stream: stream,
Temperature: props.Temperature,
TopP: props.TopP,
TopK: props.TopK,
}
}
func (c *ChatInstance) ProcessLine(data string) (*globals.Chunk, error) {
if form := processChatResponse(data); form != nil {
return &globals.Chunk{
Content: form.Delta.Text,
}, nil
}
if form := processChatErrorResponse(data); form != nil {
return &globals.Chunk{Content: ""}, fmt.Errorf("anthropic error: %s (type: %s)", form.Error.Message, form.Error.Type)
}
return &globals.Chunk{Content: ""}, nil
}
func processChatErrorResponse(data string) *ChatErrorResponse {
if form := utils.UnmarshalForm[ChatErrorResponse](data); form != nil {
return form
}
return nil
}
func processChatResponse(data string) *ChatStreamResponse {
if form := utils.UnmarshalForm[ChatStreamResponse](data); form != nil {
return form
}
return nil
}
// CreateStreamChatRequest is the stream request for anthropic claude
func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, hook globals.Hook) error {
err := utils.EventScanner(&utils.EventScannerProps{
Method: "POST",
Uri: c.GetChatEndpoint(),
Headers: c.GetChatHeaders(),
Body: c.GetChatBody(props, true),
Callback: func(data string) error {
partial, err := c.ProcessLine(data)
if err != nil {
return err
}
return hook(partial)
},
},
props.Proxy,
)
if err != nil {
if form := processChatErrorResponse(err.Body); form != nil {
if form.Error.Type == "" && form.Error.Message == "" {
return errors.New(utils.ToMarkdownCode("json", err.Body))
}
return errors.New(fmt.Sprintf("%s (type: %s)", form.Error.Message, form.Error.Type))
}
return fmt.Errorf("%s\n%s", err.Error, errors.New(utils.ToMarkdownCode("json", err.Body)))
}
return nil
}
================================================
FILE: adapter/claude/struct.go
================================================
package claude
import (
factory "chat/adapter/common"
"chat/globals"
)
type ChatInstance struct {
Endpoint string
ApiKey string
}
func NewChatInstance(endpoint, apiKey string) *ChatInstance {
return &ChatInstance{
Endpoint: endpoint,
ApiKey: apiKey,
}
}
func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {
return NewChatInstance(
conf.GetEndpoint(),
conf.GetRandomSecret(),
)
}
func (c *ChatInstance) GetEndpoint() string {
return c.Endpoint
}
func (c *ChatInstance) GetApiKey() string {
return c.ApiKey
}
================================================
FILE: adapter/claude/types.go
================================================
package claude
// ChatBody is the request body for anthropic claude
type Message struct {
Role string `json:"role"`
Content interface{} `json:"content"`
}
type MessageImage struct {
Type string `json:"type"`
MediaType interface{} `json:"media_type"`
Data interface{} `json:"data"`
}
type MessageContent struct {
Type string `json:"type"`
Text *string `json:"text,omitempty"`
Source *MessageImage `json:"source,omitempty"`
}
type ChatBody struct {
Messages []Message `json:"messages"`
MaxTokens int `json:"max_tokens"`
Model string `json:"model"`
System string `json:"system"`
Stream bool `json:"stream"`
Temperature *float32 `json:"temperature,omitempty"`
TopP *float32 `json:"top_p,omitempty"`
TopK *int `json:"top_k,omitempty"`
}
type ChatStreamResponse struct {
Type string `json:"type"`
Index int `json:"index"`
Delta struct {
Type string `json:"type"`
Text string `json:"text"`
} `json:"delta"`
}
type ChatErrorResponse struct {
Error struct {
Type string `json:"type" binding:"required"`
Message string `json:"message"`
} `json:"error"`
}
================================================
FILE: adapter/common/interface.go
================================================
package adaptercommon
import (
"chat/globals"
)
type Factory interface {
CreateStreamChatRequest(props *ChatProps, hook globals.Hook) error
}
type VideoFactory interface {
CreateVideoRequest(props *VideoProps, hook globals.Hook) error
}
type FactoryCreator func(globals.ChannelConfig) Factory
================================================
FILE: adapter/common/types.go
================================================
package adaptercommon
import (
"chat/globals"
"chat/utils"
)
type RequestProps struct {
MaxRetries *int `json:"-"`
Current int `json:"-"`
Group string `json:"-"`
Proxy globals.ProxyConfig `json:"-"`
}
type VideoProps struct {
RequestProps
Model string `json:"model,omitempty"`
OriginalModel string `json:"-"`
Prompt string `json:"prompt"`
Seconds *string `json:"seconds,omitempty"`
Size *string `json:"size,omitempty"`
InputReference *string `json:"input_reference,omitempty"`
User string `json:"-"`
}
type ChatProps struct {
RequestProps
Model string `json:"model,omitempty"`
OriginalModel string `json:"-"`
Message []globals.Message `json:"messages,omitempty"`
MaxTokens *int `json:"max_tokens,omitempty"`
PresencePenalty *float32 `json:"presence_penalty,omitempty"`
FrequencyPenalty *float32 `json:"frequency_penalty,omitempty"`
RepetitionPenalty *float32 `json:"repetition_penalty,omitempty"`
Temperature *float32 `json:"temperature,omitempty"`
TopP *float32 `json:"top_p,omitempty"`
TopK *int `json:"top_k,omitempty"`
Tools *globals.FunctionTools `json:"tools,omitempty"`
ToolChoice *interface{} `json:"tool_choice,omitempty"`
Buffer *utils.Buffer `json:"-"`
}
func (c *ChatProps) SetupBuffer(buf *utils.Buffer) {
buf.SetPrompts(c)
c.Buffer = buf
}
func CreateChatProps(props *ChatProps, buffer *utils.Buffer) *ChatProps {
props.SetupBuffer(buffer)
return props
}
func CreateVideoProps(props *VideoProps) *VideoProps {
return props
}
================================================
FILE: adapter/coze/chat.go
================================================
package coze
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
"time"
)
type ChatInstance struct {
Endpoint string
ApiKey string
AutoSaveHistory bool
responseComplete bool
}
func (c *ChatInstance) GetEndpoint() string {
return c.Endpoint
}
func (c *ChatInstance) GetApiKey() string {
return c.ApiKey
}
func (c *ChatInstance) GetHeader() map[string]string {
return map[string]string{
"Content-Type": "application/json",
"Authorization": fmt.Sprintf("Bearer %s", c.GetApiKey()),
}
}
func NewChatInstance(endpoint, apiKey string) *ChatInstance {
return &ChatInstance{
Endpoint: endpoint,
ApiKey: apiKey,
AutoSaveHistory: false,
}
}
func NewChatInstanceFromConfig(conf globals.ChannelConfig) adaptercommon.Factory {
return NewChatInstance(
conf.GetEndpoint(),
conf.GetRandomSecret(),
)
}
func (c *ChatInstance) GetChatEndpoint() string {
return fmt.Sprintf("%s/v3/chat", c.GetEndpoint())
}
func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} {
additionalMessages := []EnterMessage{}
for _, msg := range props.Message {
enterMsg := EnterMessage{
Role: msg.Role,
Content: msg.Content,
ContentType: "text",
}
if msg.Role == "user" {
enterMsg.Type = "question"
} else if msg.Role == "assistant" {
enterMsg.Type = "answer"
}
additionalMessages = append(additionalMessages, enterMsg)
}
// `user_id` is required in coze
timestamp := time.Now().UnixNano()
userID := fmt.Sprintf("user_%d", timestamp)
return ChatRequest{
BotID: props.Model,
UserID: userID,
AdditionalMessages: additionalMessages,
Stream: stream,
AutoSaveHistory: c.AutoSaveHistory,
}
}
func (c *ChatInstance) ProcessLine(data string) (string, error) {
if c.responseComplete {
return "", nil
}
if data == "" {
return "", nil
}
chunk, complete, err := processStreamResponse(data)
if err != nil {
return "", err
}
if complete {
c.responseComplete = true
}
return chunk.Content, nil
}
func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {
// TODO: use standard non-stream request
c.AutoSaveHistory = true
res, err := utils.Post(
c.GetChatEndpoint(),
c.GetHeader(),
c.GetChatBody(props, false),
props.Proxy,
)
if err != nil || res == nil {
return "", fmt.Errorf("coze error: %s", err.Error())
}
responseBody := utils.Marshal(res)
response := processChatResponse(responseBody)
if response == nil {
return "", fmt.Errorf("coze error: cannot parse response")
}
if response.Code != 0 {
return "", fmt.Errorf("coze error: %s (code: %d)", response.Msg, response.Code)
}
var responseContent string
var responseMutex sync.Mutex
err = c.CreateStreamChatRequest(props, func(chunk *globals.Chunk) error {
responseMutex.Lock()
defer responseMutex.Unlock()
responseContent += chunk.Content
return nil
})
if err != nil {
return "", err
}
if responseContent == "" {
return "", fmt.Errorf("coze error: empty response from API")
}
return responseContent, nil
}
func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {
c.responseComplete = false
c.AutoSaveHistory = false
err := utils.EventScanner(&utils.EventScannerProps{
Method: "POST",
Uri: c.GetChatEndpoint(),
Headers: c.GetHeader(),
Body: c.GetChatBody(props, true),
FullSSE: true,
Callback: func(data string) error {
partial, err := c.ProcessLine(data)
if err != nil {
return err
}
if partial != "" {
err = callback(&globals.Chunk{Content: partial})
if err != nil {
return err
}
}
return nil
},
}, props.Proxy)
c.responseComplete = true
if err != nil {
if strings.Contains(err.Body, "\"code\":") {
errorResp := processChatErrorResponse(err.Body)
if errorResp != nil && errorResp.Data.Code != 0 {
return errors.New(fmt.Sprintf("coze error: %s (code: %d)", errorResp.Data.Msg, errorResp.Data.Code))
}
var genericResp map[string]interface{}
if jsonErr := json.Unmarshal([]byte(err.Body), &genericResp); jsonErr == nil {
errMsg, _ := json.Marshal(genericResp)
return errors.New(fmt.Sprintf("coze error: %s", string(errMsg)))
}
}
if err.Error != nil {
return err.Error
}
return errors.New(fmt.Sprintf("coze error: unexpected error in stream request"))
}
return nil
}
================================================
FILE: adapter/coze/processor.go
================================================
package coze
import (
"chat/globals"
"chat/utils"
"errors"
"fmt"
"strconv"
"strings"
)
func processChatResponse(data string) *ChatResponse {
if form := utils.UnmarshalForm[ChatResponse](data); form != nil {
return form
}
return nil
}
func processChatStreamResponse(data string) *ChatStreamResponse {
if form := utils.UnmarshalForm[ChatStreamResponse](data); form != nil {
return form
}
return nil
}
func processChatStreamData(data string) *ChatStreamData {
if form := utils.UnmarshalForm[ChatStreamData](data); form != nil {
return form
}
return nil
}
func processChatErrorResponse(data string) *ChatStreamErrorResponse {
if form := utils.UnmarshalForm[ChatStreamErrorResponse](data); form != nil {
return form
}
return nil
}
func processSSEData(data string) (event string, eventData string, err error) {
if data == "" {
return "", "", nil
}
sseLines := strings.Split(data, "\n")
for _, line := range sseLines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "event:") {
event = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
} else if strings.HasPrefix(line, "data:") {
eventData = strings.TrimSpace(strings.TrimPrefix(line, "data:"))
}
}
if eventData == "" {
return "", "", nil
}
if strings.HasPrefix(eventData, "\"") && strings.HasSuffix(eventData, "\"") && len(eventData) > 2 {
unquoted, err := strconv.Unquote(eventData)
if err == nil {
eventData = unquoted
}
}
return event, eventData, nil
}
func processEventContent(event string, eventData string) (content string, complete bool, err error) {
switch event {
case "conversation.message.delta":
content, _ := parseEventContent(event, eventData)
if content != "" {
return content, false, nil
}
streamData := processChatStreamData(eventData)
if streamData != nil && streamData.Type == "answer" && streamData.Role == "assistant" && streamData.Content != "" {
return streamData.Content, false, nil
}
case "conversation.message.completed":
return "", false, nil
case "conversation.chat.completed":
return "", true, nil
case "conversation.chat.failed":
streamData := processChatStreamData(eventData)
if streamData != nil {
if streamData.Code != 0 && streamData.Msg != "" {
return "", false, errors.New(fmt.Sprintf("coze error: %s (code: %d)", streamData.Msg, streamData.Code))
}
}
return "", false, errors.New("coze error: conversation failed")
case "done":
return "", true, nil
}
errorResp := processChatErrorResponse(eventData)
if errorResp != nil && errorResp.Data.Code != 0 {
return "", false, errors.New(fmt.Sprintf("coze error: %s (code: %d)", errorResp.Data.Msg, errorResp.Data.Code))
}
streamData := processChatStreamData(eventData)
if streamData != nil {
if streamData.Code != 0 && streamData.Msg != "" {
return "", false, errors.New(fmt.Sprintf("coze error: %s (code: %d)", streamData.Msg, streamData.Code))
}
if streamData.LastError.Code != 0 && streamData.LastError.Msg != "" {
return "", false, errors.New(fmt.Sprintf("coze error: %s (code: %d)", streamData.LastError.Msg, streamData.LastError.Code))
}
}
return "", false, nil
}
func parseEventContent(eventType string, eventData string) (string, error) {
if eventType == "conversation.message.delta" {
streamResp := processChatStreamResponse(fmt.Sprintf(`{"event":"%s","data":%s}`, eventType, eventData))
if streamResp != nil {
streamData := processChatStreamData(streamResp.Data)
if streamData != nil && streamData.Type == "answer" && streamData.Role == "assistant" && streamData.Content != "" {
return streamData.Content, nil
}
}
}
return "", nil
}
func processStreamResponse(data string) (*globals.Chunk, bool, error) {
event, eventData, err := processSSEData(data)
if err != nil {
return nil, false, err
}
if event == "" || eventData == "" {
return &globals.Chunk{Content: ""}, false, nil
}
content, complete, err := processEventContent(event, eventData)
if err != nil {
return nil, false, err
}
return &globals.Chunk{
Content: content,
}, complete, nil
}
================================================
FILE: adapter/coze/struct.go
================================================
package coze
type ChatRequest struct {
BotID string `json:"bot_id"`
UserID string `json:"user_id"`
AdditionalMessages []EnterMessage `json:"additional_messages,omitempty"`
Stream bool `json:"stream"`
CustomVariables map[string]string `json:"custom_variables,omitempty"`
AutoSaveHistory bool `json:"auto_save_history"`
MetaData map[string]string `json:"meta_data,omitempty"`
ExtraParams map[string]string `json:"extra_params,omitempty"`
ShortcutCommand *ShortcutCommand `json:"shortcut_command,omitempty"`
}
type EnterMessage struct {
Role string `json:"role"`
Type string `json:"type,omitempty"`
Content string `json:"content,omitempty"`
ContentType string `json:"content_type,omitempty"`
MetaData map[string]string `json:"meta_data,omitempty"`
}
type ShortcutCommand struct {
// TODO: support for adding this on demand
}
type ObjectString struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
FileID string `json:"file_id,omitempty"`
FileURL string `json:"file_url,omitempty"`
}
type ChatResponse struct {
Data struct {
ID string `json:"id"`
ConversationID string `json:"conversation_id"`
BotID string `json:"bot_id"`
CreatedAt int64 `json:"created_at"`
CompletedAt int64 `json:"completed_at"`
LastError interface{} `json:"last_error"`
MetaData map[string]string `json:"meta_data"`
Status string `json:"status"`
Usage *Usage `json:"usage"`
} `json:"data"`
Code int `json:"code"`
Msg string `json:"msg"`
}
type Usage struct {
TokenCount int `json:"token_count"`
OutputTokens int `json:"output_tokens"`
InputTokens int `json:"input_tokens"`
}
type ChatStreamResponse struct {
Event string `json:"event"`
Data string `json:"data"`
}
type ChatStreamData struct {
ID string `json:"id,omitempty"`
Role string `json:"role,omitempty"`
Type string `json:"type,omitempty"`
Content string `json:"content,omitempty"`
ContentType string `json:"content_type,omitempty"`
ChatID string `json:"chat_id,omitempty"`
ConversationID string `json:"conversation_id,omitempty"`
BotID string `json:"bot_id,omitempty"`
SectionID string `json:"section_id,omitempty"`
CreatedAt int64 `json:"created_at,omitempty"`
CompletedAt int64 `json:"completed_at,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"`
Status string `json:"status,omitempty"`
LastError struct {
Code int `json:"code"`
Msg string `json:"msg"`
} `json:"last_error,omitempty"`
Code int `json:"code"`
Msg string `json:"msg"`
Usage *Usage `json:"usage,omitempty"`
MetaData map[string]string `json:"meta_data,omitempty"`
FromModule interface{} `json:"from_module,omitempty"`
FromUnit interface{} `json:"from_unit,omitempty"`
}
type ChatStreamErrorResponse struct {
Event string `json:"event"`
Data struct {
Code int `json:"code"`
Msg string `json:"msg"`
} `json:"data"`
}
================================================
FILE: adapter/dashscope/chat.go
================================================
package dashscope
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"fmt"
"strings"
)
const defaultMaxTokens = 1500
func (c *ChatInstance) GetHeader() map[string]string {
return map[string]string{
"Content-Type": "application/json",
"Authorization": fmt.Sprintf("Bearer %s", c.GetApiKey()),
"X-DashScope-SSE": "enable",
}
}
func (c *ChatInstance) FormatMessages(message []globals.Message) []Message {
var messages []Message
var start bool
for _, v := range message {
if v.Role == globals.Tool {
continue
}
if !start {
start = true
// dashscope first message should be [`user`, `system`] role, convert other roles to `user`
if v.Role != globals.User && v.Role != globals.System {
v.Role = globals.User
}
}
messages = append(messages, Message{
Role: v.Role,
Content: v.Content,
})
}
return messages
}
func (c *ChatInstance) GetMaxTokens(props *adaptercommon.ChatProps) int {
// dashscope has a restriction of 1500 tokens in completion
if props.MaxTokens == nil || *props.MaxTokens <= 0 || *props.MaxTokens > 1500 {
return defaultMaxTokens
}
return *props.MaxTokens
}
func (c *ChatInstance) GetTopP(props *adaptercommon.ChatProps) *float32 {
// range of top_p should be (0.0, 1.0)
if props.TopP == nil {
return nil
}
if *props.TopP <= 0.0 {
return utils.ToPtr[float32](0.1)
} else if *props.TopP >= 1.0 {
return utils.ToPtr[float32](0.9)
}
return props.TopP
}
func (c *ChatInstance) GetRepeatPenalty(props *adaptercommon.ChatProps) *float32 {
// range of repetition_penalty should greater than 0.0
if props.RepetitionPenalty == nil {
return nil
}
if *props.RepetitionPenalty <= 0.0 {
return utils.ToPtr[float32](0.1)
}
return props.RepetitionPenalty
}
func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps) ChatRequest {
return ChatRequest{
Model: strings.TrimSuffix(props.Model, "-net"),
Input: ChatInput{
Messages: c.FormatMessages(props.Message),
},
Parameters: ChatParam{
MaxTokens: c.GetMaxTokens(props),
Temperature: props.Temperature,
TopP: c.GetTopP(props),
TopK: props.TopK,
RepetitionPenalty: c.GetRepeatPenalty(props),
EnableSearch: utils.ToPtr(strings.HasSuffix(props.Model, "-net")),
IncrementalOutput: true,
},
}
}
func (c *ChatInstance) GetChatEndpoint() string {
return fmt.Sprintf("%s/api/v1/services/aigc/text-generation/generation", c.Endpoint)
}
func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {
return utils.EventSource(
"POST",
c.GetChatEndpoint(),
c.GetHeader(),
c.GetChatBody(props),
func(data string) error {
// example:
// id:1
// event:result
// :HTTP_STATUS/200
// data:{"output":{"finish_reason":"null","text":"hi"},"usage":{"total_tokens":15,"input_tokens":14,"output_tokens":1},"request_id":"08da1369-e009-9f8f-8363-54b966f80daf"}
data = strings.TrimSpace(data)
if !strings.HasPrefix(data, "data:") {
return nil
}
slice := strings.TrimSpace(strings.TrimPrefix(data, "data:"))
if form := utils.UnmarshalForm[ChatResponse](slice); form != nil {
if form.Output.Text == "" && form.Message != "" {
return fmt.Errorf("dashscope error: %s", form.Message)
}
if err := callback(&globals.Chunk{Content: form.Output.Text}); err != nil {
return err
}
return nil
}
globals.Debug(fmt.Sprintf("dashscope error: cannot unmarshal data %s", slice))
return nil
},
props.Proxy,
)
}
================================================
FILE: adapter/dashscope/struct.go
================================================
package dashscope
import (
factory "chat/adapter/common"
"chat/globals"
)
type ChatInstance struct {
Endpoint string
ApiKey string
}
func (c *ChatInstance) GetApiKey() string {
return c.ApiKey
}
func (c *ChatInstance) GetEndpoint() string {
return c.Endpoint
}
func NewChatInstance(endpoint string, apiKey string) *ChatInstance {
return &ChatInstance{
Endpoint: endpoint,
ApiKey: apiKey,
}
}
func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {
return NewChatInstance(
conf.GetEndpoint(),
conf.GetRandomSecret(),
)
}
================================================
FILE: adapter/dashscope/types.go
================================================
package dashscope
// ChatRequest is the request body for dashscope
type ChatRequest struct {
Model string `json:"model"`
Input ChatInput `json:"input"`
Parameters ChatParam `json:"parameters"`
}
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
type ChatInput struct {
Messages []Message `json:"messages"`
}
type ChatParam struct {
IncrementalOutput bool `json:"incremental_output"`
EnableSearch *bool `json:"enable_search,omitempty"`
MaxTokens int `json:"max_tokens"`
Temperature *float32 `json:"temperature,omitempty"`
TopP *float32 `json:"top_p,omitempty"`
TopK *int `json:"top_k,omitempty"`
RepetitionPenalty *float32 `json:"repetition_penalty,omitempty"`
}
// ChatResponse is the response body for dashscope
type ChatResponse struct {
Output struct {
FinishReason string `json:"finish_reason"`
Text string `json:"text"`
} `json:"output"`
RequestId string `json:"request_id"`
Usage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
} `json:"usage"`
Message string `json:"message"`
}
================================================
FILE: adapter/deepseek/chat.go
================================================
package deepseek
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"errors"
"fmt"
)
type ChatInstance struct {
Endpoint string
ApiKey string
isFirstReasoning bool
isReasonOver bool
}
func (c *ChatInstance) GetEndpoint() string {
return c.Endpoint
}
func (c *ChatInstance) GetApiKey() string {
return c.ApiKey
}
func (c *ChatInstance) GetHeader() map[string]string {
return map[string]string{
"Content-Type": "application/json",
"Authorization": fmt.Sprintf("Bearer %s", c.GetApiKey()),
}
}
func NewChatInstance(endpoint, apiKey string) *ChatInstance {
return &ChatInstance{
Endpoint: endpoint,
ApiKey: apiKey,
isFirstReasoning: true,
}
}
func NewChatInstanceFromConfig(conf globals.ChannelConfig) adaptercommon.Factory {
return NewChatInstance(
conf.GetEndpoint(),
conf.GetRandomSecret(),
)
}
func (c *ChatInstance) GetChatEndpoint() string {
return fmt.Sprintf("%s/chat/completions", c.GetEndpoint())
}
func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} {
messages := props.Message
// because of deepseek first message must be user role
// convert assistant message to user message
if len(messages) > 0 && messages[0].Role == globals.Assistant {
messages = make([]globals.Message, len(props.Message))
copy(messages, props.Message)
messages[0].Role = globals.User
}
return ChatRequest{
Model: props.Model,
Messages: messages,
MaxTokens: props.MaxTokens,
Stream: stream,
Temperature: props.Temperature,
TopP: props.TopP,
PresencePenalty: props.PresencePenalty,
FrequencyPenalty: props.FrequencyPenalty,
}
}
func processChatResponse(data string) *ChatResponse {
if form := utils.UnmarshalForm[ChatResponse](data); form != nil {
return form
}
return nil
}
func processChatStreamResponse(data string) *ChatStreamResponse {
if form := utils.UnmarshalForm[ChatStreamResponse](data); form != nil {
return form
}
return nil
}
func processChatErrorResponse(data string) *ChatStreamErrorResponse {
if form := utils.UnmarshalForm[ChatStreamErrorResponse](data); form != nil {
return form
}
return nil
}
func (c *ChatInstance) ProcessLine(data string) (string, error) {
if form := processChatStreamResponse(data); form != nil {
if len(form.Choices) == 0 {
return "", nil
}
delta := form.Choices[0].Delta
if c.isFirstReasoning == false && !c.isReasonOver && delta.ReasoningContent == nil {
c.isReasonOver = true
if delta.Content != "" {
return fmt.Sprintf("\n\n\n%s", delta.Content), nil
}
return "\n\n\n", nil
}
if delta.ReasoningContent != nil {
content := *delta.ReasoningContent
if c.isFirstReasoning {
c.isFirstReasoning = false
return fmt.Sprintf("\n%s", content), nil
}
return content, nil
}
return delta.Content, nil
}
if form := processChatErrorResponse(data); form != nil {
if form.Error.Message != "" {
return "", errors.New(fmt.Sprintf("deepseek error: %s", form.Error.Message))
}
}
return "", nil
}
func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {
res, err := utils.Post(
c.GetChatEndpoint(),
c.GetHeader(),
c.GetChatBody(props, false),
props.Proxy,
)
if err != nil || res == nil {
return "", fmt.Errorf("deepseek error: %s", err.Error())
}
data := utils.MapToStruct[ChatResponse](res)
if data == nil {
return "", fmt.Errorf("deepseek error: cannot parse response")
}
if len(data.Choices) == 0 {
return "", fmt.Errorf("deepseek error: no choices")
}
message := data.Choices[0].Message
content := message.Content
if message.ReasoningContent != nil {
content = fmt.Sprintf("\n%s\n \n\n%s", *message.ReasoningContent, content)
}
return content, nil
}
func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {
c.isFirstReasoning = true
c.isReasonOver = false
err := utils.EventScanner(&utils.EventScannerProps{
Method: "POST",
Uri: c.GetChatEndpoint(),
Headers: c.GetHeader(),
Body: c.GetChatBody(props, true),
Callback: func(data string) error {
partial, err := c.ProcessLine(data)
if err != nil {
return err
}
return callback(&globals.Chunk{Content: partial})
},
}, props.Proxy)
if err != nil {
if form := processChatErrorResponse(err.Body); form != nil {
if form.Error.Type == "" && form.Error.Message == "" {
return errors.New(utils.ToMarkdownCode("json", err.Body))
}
return errors.New(fmt.Sprintf("deepseek error: %s (type: %s)", form.Error.Message, form.Error.Type))
}
return err.Error
}
return nil
}
================================================
FILE: adapter/deepseek/struct.go
================================================
package deepseek
import (
"chat/globals"
)
// DeepSeek API is similar to OpenAI API with additional reasoning content
type ChatRequest struct {
Model string `json:"model"`
Messages []globals.Message `json:"messages"`
MaxTokens *int `json:"max_tokens,omitempty"`
Stream bool `json:"stream"`
Temperature *float32 `json:"temperature,omitempty"`
TopP *float32 `json:"top_p,omitempty"`
PresencePenalty *float32 `json:"presence_penalty,omitempty"`
FrequencyPenalty *float32 `json:"frequency_penalty,omitempty"`
}
// ChatResponse is the native http request body for deepseek
type ChatResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []struct {
Index int `json:"index"`
Message globals.Message `json:"message"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
} `json:"usage"`
}
// ChatStreamResponse is the stream response body for deepseek
type ChatStreamResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []struct {
Delta globals.Message `json:"delta"`
Index int `json:"index"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
}
type ChatStreamErrorResponse struct {
Error struct {
Message string `json:"message"`
Type string `json:"type"`
} `json:"error"`
}
================================================
FILE: adapter/dify/chat.go
================================================
package dify
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
)
type ChatInstance struct {
Endpoint string
ApiKey string
responseComplete bool
}
func (c *ChatInstance) GetEndpoint() string {
return c.Endpoint
}
func (c *ChatInstance) GetApiKey() string {
return c.ApiKey
}
func (c *ChatInstance) GetHeader() map[string]string {
return map[string]string{
"Content-Type": "application/json",
"Authorization": fmt.Sprintf("Bearer %s", c.GetApiKey()),
}
}
func NewChatInstance(endpoint, apiKey string) *ChatInstance {
return &ChatInstance{
Endpoint: endpoint,
ApiKey: apiKey,
}
}
func NewChatInstanceFromConfig(conf globals.ChannelConfig) adaptercommon.Factory {
return NewChatInstance(
conf.GetEndpoint(),
conf.GetRandomSecret(),
)
}
func (c *ChatInstance) GetChatEndpoint() string {
return fmt.Sprintf("%s/chat-messages", c.GetEndpoint())
}
func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} {
timestamp := time.Now().UnixNano()
userID := fmt.Sprintf("user_%d", timestamp)
query := ""
for _, msg := range props.Message {
if msg.Role == "user" {
query = msg.Content
break
}
}
return ChatRequest{
Inputs: map[string]interface{}{},
Query: query,
ResponseMode: "streaming",
User: userID,
AutoGenerateName: true,
}
}
func (c *ChatInstance) ProcessLine(data string) (string, error) {
if c.responseComplete {
return "", nil
}
if data == "" {
return "", nil
}
chunk, complete, err := processStreamResponse(data)
if err != nil {
return "", err
}
if complete {
c.responseComplete = true
}
return chunk.Content, nil
}
func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {
res, err := utils.Post(
c.GetChatEndpoint(),
c.GetHeader(),
c.GetChatBody(props, false),
props.Proxy,
)
if err != nil || res == nil {
return "", fmt.Errorf("dify error: %s", err.Error())
}
responseBody := utils.Marshal(res)
response := processChatResponse(responseBody)
if response == nil {
return "", fmt.Errorf("dify error: cannot parse response")
}
return response.Answer, nil
}
func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {
c.responseComplete = false
err := utils.EventScanner(&utils.EventScannerProps{
Method: "POST",
Uri: c.GetChatEndpoint(),
Headers: c.GetHeader(),
Body: c.GetChatBody(props, true),
Callback: func(data string) error {
partial, err := c.ProcessLine(data)
if err != nil {
return err
}
if partial != "" {
err = callback(&globals.Chunk{Content: partial})
if err != nil {
return err
}
}
return nil
},
}, props.Proxy)
c.responseComplete = true
if err != nil {
if strings.Contains(err.Body, "\"code\":") {
errorResp := processChatErrorResponse(err.Body)
if errorResp != nil {
return errors.New(fmt.Sprintf("dify error: %s (code: %s)", errorResp.Message, errorResp.Code))
}
var genericResp map[string]interface{}
if jsonErr := json.Unmarshal([]byte(err.Body), &genericResp); jsonErr == nil {
errMsg, _ := json.Marshal(genericResp)
return errors.New(fmt.Sprintf("dify error: %s", string(errMsg)))
}
}
if err.Error != nil {
return err.Error
}
return errors.New(fmt.Sprintf("dify error: unexpected error in stream request"))
}
return nil
}
================================================
FILE: adapter/dify/processor.go
================================================
package dify
import (
"chat/globals"
"chat/utils"
"errors"
"fmt"
)
func processChatResponse(data string) *ChatResponse {
if form := utils.UnmarshalForm[ChatResponse](data); form != nil {
return form
}
return nil
}
func processChatStreamResponse(data string) *ChatStreamResponse {
if form := utils.UnmarshalForm[ChatStreamResponse](data); form != nil {
return form
}
return nil
}
func processChatErrorResponse(data string) *ChatStreamErrorResponse {
if form := utils.UnmarshalForm[ChatStreamErrorResponse](data); form != nil {
return form
}
return nil
}
func processStreamResponse(data string) (*globals.Chunk, bool, error) {
if data == "" {
return &globals.Chunk{Content: ""}, false, nil
}
streamData := processChatStreamResponse(data)
if streamData == nil {
return &globals.Chunk{Content: ""}, false, nil
}
switch streamData.Event {
case "message":
if streamData.Answer != "" {
return &globals.Chunk{
Content: streamData.Answer,
}, false, nil
}
case "message_end":
return &globals.Chunk{
Content: "",
}, true, nil
case "error":
if streamData.Code != "" && streamData.Message != "" {
return nil, false, errors.New(fmt.Sprintf("dify error: %s (code: %s)", streamData.Message, streamData.Code))
}
return nil, false, errors.New("dify error: conversation failed")
case "workflow_started", "node_started", "node_finished", "workflow_finished", "iteration_started", "iteration_next", "iteration_finished", "iteration_completed", "parallel_branch_started", "parallel_branch_finished", "ping":
return &globals.Chunk{Content: ""}, false, nil
}
errorResp := processChatErrorResponse(data)
if errorResp != nil {
return nil, false, errors.New(fmt.Sprintf("dify error: %s (code: %s)", errorResp.Message, errorResp.Code))
}
return &globals.Chunk{Content: ""}, false, nil
}
================================================
FILE: adapter/dify/struct.go
================================================
package dify
type ChatRequest struct {
Inputs map[string]interface{} `json:"inputs"`
Query string `json:"query"`
ResponseMode string `json:"response_mode"`
ConversationID string `json:"conversation_id,omitempty"`
User string `json:"user"`
Files []File `json:"files,omitempty"`
AutoGenerateName bool `json:"auto_generate_name,omitempty"`
}
type File struct {
Type string `json:"type"`
TransferMethod string `json:"transfer_method"`
URL string `json:"url,omitempty"`
UploadFileID string `json:"upload_file_id,omitempty"`
}
type ChatResponse struct {
MessageID string `json:"message_id"`
ConversationID string `json:"conversation_id"`
Mode string `json:"mode"`
Answer string `json:"answer"`
Metadata map[string]interface{} `json:"metadata"`
Usage Usage `json:"usage"`
RetrieverResources []RetrieverResource `json:"retriever_resources"`
CreatedAt int64 `json:"created_at"`
}
type Usage struct {
TokenCount int `json:"token_count"`
OutputTokens int `json:"output_tokens"`
InputTokens int `json:"input_tokens"`
}
type RetrieverResource struct {
SegmentID string `json:"segment_id"`
Content string `json:"content"`
Source string `json:"source"`
}
type ChatStreamResponse struct {
Event string `json:"event"`
TaskID string `json:"task_id"`
MessageID string `json:"message_id,omitempty"`
ConversationID string `json:"conversation_id,omitempty"`
Answer string `json:"answer,omitempty"`
CreatedAt int64 `json:"created_at,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
Usage *Usage `json:"usage,omitempty"`
RetrieverResources []RetrieverResource `json:"retriever_resources,omitempty"`
Audio string `json:"audio,omitempty"`
Status int `json:"status,omitempty"`
Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
type ChatStreamErrorResponse struct {
Event string `json:"event"`
TaskID string `json:"task_id"`
MessageID string `json:"message_id"`
Status int `json:"status"`
Code string `json:"code"`
Message string `json:"message"`
}
================================================
FILE: adapter/hunyuan/chat.go
================================================
package hunyuan
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"context"
"fmt"
)
func (c *ChatInstance) FormatMessages(messages []globals.Message) []globals.Message {
var result []globals.Message
for _, message := range messages {
switch message.Role {
case globals.System:
result = append(result, globals.Message{Role: globals.User, Content: message.Content})
case globals.Assistant, globals.User:
bound := len(result) > 0 && result[len(result)-1].Role == message.Role
if bound {
result[len(result)-1].Content += message.Content
} else {
result = append(result, message)
}
case globals.Tool:
continue
default:
result = append(result, message)
}
}
return result
}
func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {
credential := NewCredential(c.GetSecretId(), c.GetSecretKey())
client := NewInstance(c.GetAppId(), c.GetEndpoint(), credential)
channel, err := client.Chat(context.Background(), NewRequest(Stream, c.FormatMessages(props.Message), props.Temperature, props.TopP))
if err != nil {
return fmt.Errorf("tencent hunyuan error: %+v", err)
}
for chunk := range channel {
if chunk.Error.Code != 0 {
fmt.Printf("tencent hunyuan error: %+v\n", chunk.Error)
break
}
if len(chunk.Choices) == 0 {
continue
}
choice := chunk.Choices[0].Delta
if err := callback(&globals.Chunk{Content: choice.Content}); err != nil {
return err
}
}
return nil
}
================================================
FILE: adapter/hunyuan/sdk.go
================================================
package hunyuan
/*
* Copyright (c) 2017-2018 THL A29 Limited, a Tencent company. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import (
"bufio"
"bytes"
"chat/globals"
"context"
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/google/uuid"
"io"
"net/http"
"sort"
"strconv"
"strings"
"time"
)
const (
defaultProtocol = "https"
defaultHost = "hunyuan.cloud.tencent.com"
path = "/hyllm/v1/chat/completions?"
)
const (
Synchronize = iota
Stream
)
func getUrl(endpoint string) string {
return fmt.Sprintf("%s://%s%s", getProtocol(endpoint), getHost(endpoint), path)
}
func getProtocol(endpoint string) string {
seg := strings.Split(endpoint, "://")
if len(seg) > 0 && seg[0] != "" {
return seg[0]
}
return defaultProtocol
}
func getHost(endpoint string) string {
seg := strings.Split(endpoint, "://")
if len(seg) > 1 && seg[1] != "" {
return seg[1]
}
return defaultHost
}
func getFullPath(endpoint string) string {
return getHost(endpoint) + path
}
type ResponseChoices struct {
FinishReason string `json:"finish_reason,omitempty"`
Messages []globals.Message `json:"messages,omitempty"`
Delta globals.Message `json:"delta,omitempty"`
}
type ResponseUsage struct {
PromptTokens int64 `json:"prompt_tokens,omitempty"`
TotalTokens int64 `json:"total_tokens,omitempty"`
CompletionTokens int64 `json:"completion_tokens,omitempty"`
}
type ResponseError struct {
Message string `json:"message,omitempty"`
Code int `json:"code,omitempty"`
}
type StreamDelta struct {
Content string `json:"content"`
}
type ChatRequest struct {
AppID int64 `json:"app_id"`
SecretID string `json:"secret_id"`
Timestamp int `json:"timestamp"`
Expired int `json:"expired"`
QueryID string `json:"query_id"`
Temperature float64 `json:"temperature"`
TopP float64 `json:"top_p"`
Stream int `json:"stream"`
Messages []globals.Message `json:"messages"`
}
type ChatResponse struct {
Choices []ResponseChoices `json:"choices,omitempty"`
Created string `json:"created,omitempty"`
ID string `json:"id,omitempty"`
Usage ResponseUsage `json:"usage,omitempty"`
Error ResponseError `json:"error,omitempty"`
Note string `json:"note,omitempty"`
ReqID string `json:"req_id,omitempty"`
}
type Credential struct {
SecretID string
SecretKey string
}
func NewCredential(secretID, secretKey string) *Credential {
return &Credential{SecretID: secretID, SecretKey: secretKey}
}
type Client struct {
Credential *Credential
AppID int64
EndPoint string
}
func NewInstance(appId int64, endpoint string, credential *Credential) *Client {
return &Client{
Credential: credential,
AppID: appId,
EndPoint: endpoint,
}
}
func NewRequest(mod int, messages []globals.Message, temperature *float32, topP *float32) ChatRequest {
queryID := uuid.NewString()
return ChatRequest{
Timestamp: int(time.Now().Unix()),
Expired: int(time.Now().Unix()) + 24*60*60,
Temperature: 0,
TopP: 0.8,
Messages: messages,
QueryID: queryID,
Stream: mod,
}
}
func (t *Client) getHttpReq(ctx context.Context, req ChatRequest) (*http.Request, error) {
req.AppID = t.AppID
req.SecretID = t.Credential.SecretID
signatureUrl := t.buildURL(req)
signature := t.genSignature(signatureUrl)
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("json marshal err: %+v", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", getUrl(t.EndPoint), bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("new http request err: %+v", err)
}
httpReq.Header.Set("Authorization", signature)
httpReq.Header.Set("Content-Type", "application/json")
if req.Stream == Stream {
httpReq.Header.Set("Cache-Control", "no-cache")
httpReq.Header.Set("Connection", "keep-alive")
httpReq.Header.Set("Accept", "text/event-Stream")
}
return httpReq, nil
}
func (t *Client) Chat(ctx context.Context, req ChatRequest) (<-chan ChatResponse, error) {
res := make(chan ChatResponse, 1)
httpReq, err := t.getHttpReq(ctx, req)
if err != nil {
return nil, fmt.Errorf("do general http request err: %+v", err)
}
httpResp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("do chat request err: %+v", err)
}
if httpResp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("do chat request failed status code :%d", httpResp.StatusCode)
}
if req.Stream == Synchronize {
err = t.synchronize(httpResp, res)
return res, err
}
go t.stream(httpResp, res)
return res, nil
}
func (t *Client) synchronize(httpResp *http.Response, res chan ChatResponse) (err error) {
defer func() {
httpResp.Body.Close()
close(res)
}()
var chatResp ChatResponse
respBody, err := io.ReadAll(httpResp.Body)
if err != nil {
return fmt.Errorf("read response body err: %+v", err)
}
if err = json.Unmarshal(respBody, &chatResp); err != nil {
return fmt.Errorf("json unmarshal err: %+v", err)
}
res <- chatResp
return
}
func (t *Client) stream(httpResp *http.Response, res chan ChatResponse) {
defer func() {
httpResp.Body.Close()
close(res)
}()
reader := bufio.NewReader(httpResp.Body)
for {
raw, err := reader.ReadBytes('\n')
if err != nil {
if err == io.EOF {
return
}
res <- ChatResponse{Error: ResponseError{Message: fmt.Sprintf("tencent error: read stream data failed: %+v", err), Code: 500}}
return
}
data := strings.TrimSpace(string(raw))
if data == "" || !strings.HasPrefix(data, "data: ") {
continue
}
var chatResponse ChatResponse
if err := json.Unmarshal([]byte(data[6:]), &chatResponse); err != nil {
res <- ChatResponse{Error: ResponseError{Message: fmt.Sprintf("json unmarshal err: %+v", err), Code: 500}}
return
}
res <- chatResponse
if chatResponse.Choices[0].FinishReason == "stop" {
return
}
}
}
func (t *Client) genSignature(url string) string {
mac := hmac.New(sha1.New, []byte(t.Credential.SecretKey))
signURL := url
mac.Write([]byte(signURL))
sign := mac.Sum([]byte(nil))
return base64.StdEncoding.EncodeToString(sign)
}
func (t *Client) getMessages(messages []globals.Message) string {
var message string
for _, msg := range messages {
message += fmt.Sprintf(`{"role":"%s","content":"%s"},`, msg.Role, msg.Content)
}
message = strings.TrimSuffix(message, ",")
return message
}
func (t *Client) buildURL(req ChatRequest) string {
params := make([]string, 0)
params = append(params, "app_id="+strconv.FormatInt(req.AppID, 10))
params = append(params, "secret_id="+req.SecretID)
params = append(params, "timestamp="+strconv.Itoa(req.Timestamp))
params = append(params, "query_id="+req.QueryID)
params = append(params, "temperature="+strconv.FormatFloat(req.Temperature, 'f', -1, 64))
params = append(params, "top_p="+strconv.FormatFloat(req.TopP, 'f', -1, 64))
params = append(params, "stream="+strconv.Itoa(req.Stream))
params = append(params, "expired="+strconv.Itoa(req.Expired))
params = append(params, fmt.Sprintf("messages=[%s]", t.getMessages(req.Messages)))
sort.Sort(sort.StringSlice(params))
return getFullPath(t.EndPoint) + strings.Join(params, "&")
}
================================================
FILE: adapter/hunyuan/struct.go
================================================
package hunyuan
import (
factory "chat/adapter/common"
"chat/globals"
"chat/utils"
)
type ChatInstance struct {
Endpoint string
AppId int64
SecretId string
SecretKey string
}
func (c *ChatInstance) GetAppId() int64 {
return c.AppId
}
func (c *ChatInstance) GetEndpoint() string {
return c.Endpoint
}
func (c *ChatInstance) GetSecretId() string {
return c.SecretId
}
func (c *ChatInstance) GetSecretKey() string {
return c.SecretKey
}
func NewChatInstance(endpoint, appId, secretId, secretKey string) *ChatInstance {
return &ChatInstance{
Endpoint: endpoint,
AppId: utils.ParseInt64(appId),
SecretId: secretId,
SecretKey: secretKey,
}
}
func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {
params := conf.SplitRandomSecret(3)
return NewChatInstance(
conf.GetEndpoint(),
params[0], params[1], params[2],
)
}
================================================
FILE: adapter/midjourney/api.go
================================================
package midjourney
import (
"chat/globals"
"chat/utils"
"fmt"
)
func (c *ChatInstance) GetImagineEndpoint() string {
return fmt.Sprintf("%s/mj/submit/imagine", c.GetEndpoint())
}
func (c *ChatInstance) GetChangeEndpoint() string {
return fmt.Sprintf("%s/mj/submit/change", c.GetEndpoint())
}
func (c *ChatInstance) GetImagineRequest(prompt string) *ImagineRequest {
return &ImagineRequest{
NotifyHook: c.GetNotifyEndpoint(),
Prompt: prompt,
}
}
func (c *ChatInstance) GetChangeRequest(action string, task string, index *int) *ChangeRequest {
return &ChangeRequest{
NotifyHook: c.GetNotifyEndpoint(),
Action: action,
Index: index,
TaskId: task,
}
}
func (c *ChatInstance) CreateImagineRequest(proxy globals.ProxyConfig, prompt string) (*CommonResponse, error) {
content, err := utils.PostRaw(
c.GetImagineEndpoint(),
c.GetMidjourneyHeaders(),
c.GetImagineRequest(prompt),
proxy,
)
if err != nil {
return nil, err
}
if data, err := utils.UnmarshalString[CommonResponse](content); err == nil {
return &data, nil
} else {
return nil, utils.ToMarkdownError(err, content)
}
}
func (c *ChatInstance) CreateChangeRequest(proxy globals.ProxyConfig, action string, task string, index *int) (*CommonResponse, error) {
res, err := utils.Post(
c.GetChangeEndpoint(),
c.GetMidjourneyHeaders(),
c.GetChangeRequest(action, task, index),
proxy,
)
if err != nil {
return nil, err
}
return utils.MapToStruct[CommonResponse](res), nil
}
================================================
FILE: adapter/midjourney/chat.go
================================================
package midjourney
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"fmt"
"strings"
)
const maxActions = 4
const (
ImagineAction = "IMAGINE"
UpscaleAction = "UPSCALE"
VariationAction = "VARIATION"
RerollAction = "REROLL"
)
const (
ImagineCommand = "/IMAGINE"
UpscaleCommand = "/UPSCALE"
VariationCommand = "/VARIATION"
RerollCommand = "/REROLL"
)
type ChatProps struct {
Messages []globals.Message
Model string
}
func getMode(model string) string {
switch model {
case globals.Midjourney: // relax
return RelaxMode
case globals.MidjourneyFast: // fast
return FastMode
case globals.MidjourneyTurbo: // turbo
return TurboMode
default:
return RelaxMode
}
}
func (c *ChatInstance) IsIgnoreMode() bool {
return strings.HasSuffix(c.Endpoint, "/mj-relax") ||
strings.HasSuffix(c.Endpoint, "/mj-fast") ||
strings.HasSuffix(c.Endpoint, "/mj-turbo")
}
func (c *ChatInstance) GetCleanPrompt(model string, prompt string) string {
if c.IsIgnoreMode() {
return prompt
}
arr := strings.Split(strings.TrimSpace(prompt), " ")
var res []string
for _, word := range arr {
if utils.Contains[string](word, RendererMode) {
continue
}
res = append(res, word)
}
res = append(res, getMode(model))
target := strings.Join(res, " ")
return target
}
func (c *ChatInstance) GetPrompt(props *adaptercommon.ChatProps) string {
if len(props.Message) == 0 {
return ""
}
content := props.Message[len(props.Message)-1].Content
return c.GetCleanPrompt(props.Model, content)
}
func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {
// partial response like:
// ```progress
// 0
// ...
// 100
// ```
// 
if len(globals.NotifyUrl) == 0 {
return fmt.Errorf("format error: please provide available notify url")
}
action, prompt := c.ExtractPrompt(c.GetPrompt(props))
if len(prompt) == 0 {
return fmt.Errorf("format error: please provide available prompt")
}
var begin bool
form, err := c.CreateStreamTask(props, action, prompt, func(form *StorageForm, progress int) error {
if progress == -1 {
// ping event
return callback(&globals.Chunk{Content: ""})
}
if !begin {
begin = true
if err := callback(&globals.Chunk{Content: "```progress\n"}); err != nil {
return err
}
} else if progress == 100 && !begin {
if err := callback(&globals.Chunk{Content: "```progress\n"}); err != nil {
return err
}
}
if err := callback(&globals.Chunk{Content: fmt.Sprintf("%d\n", progress)}); err != nil {
return err
}
if progress == 100 {
if err := callback(&globals.Chunk{Content: "```\n"}); err != nil {
return err
}
}
return nil
})
if err != nil {
return fmt.Errorf("error from midjourney: %s", err.Error())
}
if err := callback(&globals.Chunk{Content: utils.GetImageMarkdown(form.Url)}); err != nil {
return err
}
return c.CallbackActions(props, form, callback)
}
func toVirtualMessage(message string, model string) string {
prompt := strings.Replace(message, " ", "-", -1)
return fmt.Sprintf("https://chatnio.virtual%s::%s", prompt, model)
}
func (c *ChatInstance) CallbackActions(props *adaptercommon.ChatProps, form *StorageForm, callback globals.Hook) error {
if form.Action == UpscaleAction {
return nil
}
actions := utils.Range(1, maxActions+1)
upscale := strings.Join(utils.Each(actions, func(index int) string {
return fmt.Sprintf("[U%d](%s)", index, toVirtualMessage(fmt.Sprintf("/UPSCALE %s %d", form.Task, index), props.OriginalModel))
}), " ")
variation := strings.Join(utils.Each(actions, func(index int) string {
return fmt.Sprintf("[V%d](%s)", index, toVirtualMessage(fmt.Sprintf("/VARIATION %s %d", form.Task, index), props.OriginalModel))
}), " ")
reroll := fmt.Sprintf("[REROLL](%s)", toVirtualMessage(fmt.Sprintf("/REROLL %s", form.Task), props.OriginalModel))
return callback(&globals.Chunk{
Content: fmt.Sprintf("\n\n%s\n\n%s\n\n%s\n", upscale, variation, reroll),
})
}
================================================
FILE: adapter/midjourney/expose.go
================================================
package midjourney
import (
"chat/globals"
"chat/utils"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"strings"
)
var whiteList []string
func SaveWhiteList(raw string) {
arr := utils.Filter(strings.Split(raw, ","), func(s string) bool {
return len(strings.TrimSpace(s)) > 0
})
for _, ip := range arr {
if !utils.Contains(ip, whiteList) {
whiteList = append(whiteList, ip)
}
}
}
func InWhiteList(ip string) bool {
if len(whiteList) == 0 {
return true
}
return utils.Contains(ip, whiteList)
}
func NotifyAPI(c *gin.Context) {
if !InWhiteList(c.ClientIP()) {
globals.Info(fmt.Sprintf("[midjourney] notify api: banned request from %s", c.ClientIP()))
c.AbortWithStatus(http.StatusForbidden)
return
}
var form NotifyForm
if err := c.ShouldBindJSON(&form); err != nil {
c.AbortWithStatus(http.StatusBadRequest)
return
}
globals.Debug(fmt.Sprintf("[midjourney] notify api: get notify: %s (from: %s)", utils.Marshal(form), c.ClientIP()))
if !utils.Contains(form.Status, []string{InProgress, Success, Failure}) {
// ignore
return
}
reason, ok := form.FailReason.(string)
if !ok {
reason = "unknown"
}
err := setStorage(form.Id, StorageForm{
Task: form.Id,
Action: form.Action,
Url: form.ImageUrl,
FailReason: reason,
Progress: form.Progress,
Status: form.Status,
})
c.JSON(http.StatusOK, gin.H{
"status": err == nil,
})
}
================================================
FILE: adapter/midjourney/handler.go
================================================
package midjourney
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"fmt"
"strings"
"time"
)
const maxTimeout = 30 * time.Minute // 30 min timeout
func getStatusCode(action string, response *CommonResponse) error {
code := response.Code
switch code {
case SuccessCode, QueueCode:
return nil
case ExistedCode:
if action != ImagineCommand {
return nil
}
return fmt.Errorf("task is existed, please try again later with another prompt")
case MaxQueueCode:
return fmt.Errorf("task queue is full, please try again later")
case NudeCode:
return fmt.Errorf("prompt violates the content policy of midjourney, the request is rejected")
default:
return fmt.Errorf(fmt.Sprintf("unknown error from midjourney (code: %d, description: %s)", code, response.Description))
}
}
func getProgress(value string) int {
progress := strings.TrimSuffix(value, "%")
return utils.ParseInt(progress)
}
func (c *ChatInstance) GetAction(command string) string {
return strings.TrimLeft(command, "/")
}
func (c *ChatInstance) ExtractPrompt(input string) (action string, prompt string) {
segment := utils.SafeSplit(input, " ", 2)
action = strings.TrimSpace(segment[0])
prompt = strings.TrimSpace(segment[1])
switch action {
case ImagineCommand, VariationCommand, UpscaleCommand, RerollCommand:
return
default:
return ImagineCommand, strings.TrimSpace(input)
}
}
func (c *ChatInstance) ExtractCommand(input string) (task string, index *int) {
segment := utils.SafeSplit(input, " ", 2)
task = strings.TrimSpace(segment[0])
if segment[1] != "" {
data := segment[1]
slice := strings.Split(segment[1], " ")
if len(slice) > 1 {
data = slice[0]
}
index = utils.ToPtr(utils.ParseInt(strings.TrimSpace(data)))
}
return
}
func (c *ChatInstance) CreateRequest(proxy globals.ProxyConfig, action string, prompt string) (*CommonResponse, error) {
switch action {
case ImagineCommand:
return c.CreateImagineRequest(proxy, prompt)
case VariationCommand, UpscaleCommand, RerollCommand:
task, index := c.ExtractCommand(prompt)
return c.CreateChangeRequest(proxy, c.GetAction(action), task, index)
default:
return nil, fmt.Errorf("unknown action: %s", action)
}
}
func (c *ChatInstance) CreateStreamTask(props *adaptercommon.ChatProps, action string, prompt string, hook func(form *StorageForm, progress int) error) (*StorageForm, error) {
res, err := c.CreateRequest(props.Proxy, action, prompt)
if err != nil {
return nil, err
}
if err := getStatusCode(action, res); err != nil {
return nil, err
}
task := res.Result
progress := -1
ticker := time.NewTicker(50 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
form := getNotifyStorage(task)
if form == nil {
// hook for ping (in order to catch the stop signal)
if err := hook(nil, -1); err != nil {
return nil, err
}
continue
}
switch form.Status {
case Success:
if err := hook(form, 100); err != nil {
return nil, err
}
return form, nil
case Failure:
return nil, fmt.Errorf("task failed: %s", form.FailReason)
case InProgress:
current := getProgress(form.Progress)
if progress != current {
if err := hook(form, current); err != nil {
return nil, err
}
progress = current
}
default:
// ping
if err := hook(form, -1); err != nil {
return nil, err
}
}
case <-time.After(maxTimeout):
return nil, fmt.Errorf("task timeout")
}
}
}
================================================
FILE: adapter/midjourney/storage.go
================================================
package midjourney
import (
"chat/connection"
"chat/utils"
"fmt"
)
func getTaskName(task string) string {
return fmt.Sprintf("nio:mj-task:%s", task)
}
func setStorage(task string, form StorageForm) error {
return utils.SetJson(connection.Cache, getTaskName(task), form, 60*60)
}
func getNotifyStorage(task string) *StorageForm {
return utils.GetCacheStore[StorageForm](connection.Cache, getTaskName(task))
}
================================================
FILE: adapter/midjourney/struct.go
================================================
package midjourney
import (
factory "chat/adapter/common"
"chat/globals"
"fmt"
)
var midjourneyEmptySecret = "null"
type ChatInstance struct {
Endpoint string
ApiSecret string
}
func (c *ChatInstance) GetApiSecret() string {
return c.ApiSecret
}
func (c *ChatInstance) GetEndpoint() string {
return c.Endpoint
}
func (c *ChatInstance) GetMidjourneyHeaders() map[string]string {
secret := c.GetApiSecret()
if secret == "" || secret == midjourneyEmptySecret {
return map[string]string{
"Content-Type": "application/json",
}
}
return map[string]string{
"Content-Type": "application/json",
"mj-api-secret": secret,
}
}
func (c *ChatInstance) GetNotifyEndpoint() string {
return fmt.Sprintf("%s/mj/notify", globals.NotifyUrl)
}
func NewChatInstance(endpoint, apiSecret, whiteList string) *ChatInstance {
SaveWhiteList(whiteList)
return &ChatInstance{
Endpoint: endpoint,
ApiSecret: apiSecret,
}
}
func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {
params := conf.SplitRandomSecret(2)
return NewChatInstance(
conf.GetEndpoint(),
params[0], params[1],
)
}
================================================
FILE: adapter/midjourney/types.go
================================================
package midjourney
const (
SuccessCode = 1
ExistedCode = 21
QueueCode = 22
MaxQueueCode = 23
NudeCode = 24
)
const (
NotStartStatus = "NOT_START"
Submitted = "SUBMITTED"
InProgress = "IN_PROGRESS"
Failure = "FAILURE"
Success = "SUCCESS"
)
const (
TurboMode = "--turbo"
FastMode = "--fast"
RelaxMode = "--relax"
)
var RendererMode = []string{TurboMode, FastMode, RelaxMode}
type CommonHeader struct {
ContentType string `json:"Content-Type"`
MjApiSecret string `json:"mj-api-secret,omitempty"`
}
type CommonResponse struct {
Code int `json:"code"`
Description string `json:"description"`
Result string `json:"result"`
}
type ImagineRequest struct {
NotifyHook string `json:"notifyHook"`
Prompt string `json:"prompt"`
}
type ChangeRequest struct {
NotifyHook string `json:"notifyHook"`
Action string `json:"action"`
Index *int `json:"index,omitempty"`
TaskId string `json:"taskId"`
}
type NotifyForm struct {
Id string `json:"id"`
Action string `json:"action"`
Status string `json:"status"`
Prompt string `json:"prompt"`
PromptEn string `json:"promptEn"`
Description string `json:"description"`
SubmitTime int64 `json:"submitTime"`
StartTime int64 `json:"startTime"`
FinishTime int64 `json:"finishTime"`
Progress string `json:"progress"`
ImageUrl string `json:"imageUrl"`
FailReason interface{} `json:"failReason"`
}
type StorageForm struct {
Task string `json:"task"`
Action string `json:"action"`
Url string `json:"url"`
FailReason string `json:"failReason"`
Progress string `json:"progress"`
Status string `json:"status"`
}
================================================
FILE: adapter/openai/chat.go
================================================
package openai
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"errors"
"fmt"
"regexp"
"strings"
)
func (c *ChatInstance) GetChatEndpoint(props *adaptercommon.ChatProps) string {
if props.Model == globals.GPT3TurboInstruct {
return fmt.Sprintf("%s/v1/completions", c.GetEndpoint())
}
return fmt.Sprintf("%s/v1/chat/completions", c.GetEndpoint())
}
func (c *ChatInstance) GetCompletionPrompt(messages []globals.Message) string {
result := ""
for _, message := range messages {
result += fmt.Sprintf("%s: %s\n", message.Role, message.Content)
}
return result
}
func (c *ChatInstance) GetLatestPrompt(props *adaptercommon.ChatProps) string {
if len(props.Message) == 0 {
return ""
}
return props.Message[len(props.Message)-1].Content
}
func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} {
if props.Model == globals.GPT3TurboInstruct {
// for completions
return CompletionRequest{
Model: props.Model,
Prompt: c.GetCompletionPrompt(props.Message),
MaxToken: props.MaxTokens,
Stream: stream,
}
}
messages := formatMessages(props)
// o1, o3, gpt-5 compatibility
isNewModel := len(props.Model) >= 2 && (props.Model[:2] == "o1" || props.Model[:2] == "o3") || strings.HasPrefix(props.Model, "gpt-5")
var temperature *float32
if isNewModel {
temp := float32(1.0)
temperature = &temp
} else {
temperature = props.Temperature
}
request := ChatRequest{
Model: props.Model,
Messages: messages,
Stream: stream,
PresencePenalty: props.PresencePenalty,
FrequencyPenalty: props.FrequencyPenalty,
Temperature: temperature,
TopP: props.TopP,
Tools: props.Tools,
ToolChoice: props.ToolChoice,
}
if isNewModel {
request.MaxCompletionTokens = props.MaxTokens
} else {
request.MaxToken = props.MaxTokens
}
return request
}
// CreateChatRequest is the native http request body for openai
func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {
if globals.IsOpenAIDalleModel(props.Model) {
return c.CreateImage(props)
}
res, err := utils.Post(
c.GetChatEndpoint(props),
c.GetHeader(),
c.GetChatBody(props, false),
props.Proxy,
)
if err != nil || res == nil {
return "", fmt.Errorf("openai error: %s", err.Error())
}
data := utils.MapToStruct[ChatResponse](res)
if data == nil {
return "", fmt.Errorf("openai error: cannot parse response")
} else if data.Error.Message != "" {
return "", fmt.Errorf("openai error: %s", data.Error.Message)
}
return data.Choices[0].Message.Content, nil
}
func hideRequestId(message string) string {
// xxx (request id: 2024020311120561344953f0xfh0TX)
exp := regexp.MustCompile(`\(request id: [a-zA-Z0-9]+\)`)
return exp.ReplaceAllString(message, "")
}
// CreateStreamChatRequest is the stream response body for openai
func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {
if globals.IsOpenAIDalleModel(props.Model) {
if url, err := c.CreateImage(props); err != nil {
return err
} else {
return callback(&globals.Chunk{
Content: url,
})
}
}
isCompletionType := props.Model == globals.GPT3TurboInstruct
ticks := 0
err := utils.EventScanner(&utils.EventScannerProps{
Method: "POST",
Uri: c.GetChatEndpoint(props),
Headers: c.GetHeader(),
Body: c.GetChatBody(props, true),
Callback: func(data string) error {
ticks += 1
partial, err := c.ProcessLine(data, isCompletionType)
if err != nil {
return err
}
return callback(partial)
},
}, props.Proxy)
if err != nil {
if form := processChatErrorResponse(err.Body); form != nil {
if form.Error.Type == "" && form.Error.Message == "" {
return errors.New(utils.ToMarkdownCode("json", err.Body))
}
msg := fmt.Sprintf("%s (type: %s)", form.Error.Message, form.Error.Type)
return errors.New(hideRequestId(msg))
}
return err.Error
}
if ticks == 0 {
return errors.New("no response")
}
return nil
}
================================================
FILE: adapter/openai/image.go
================================================
package openai
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"fmt"
"strings"
)
type ImageProps struct {
Model string
Prompt string
Size ImageSize
Proxy globals.ProxyConfig
}
func (c *ChatInstance) GetImageEndpoint() string {
return fmt.Sprintf("%s/v1/images/generations", c.GetEndpoint())
}
// CreateImageRequest will create a dalle image from prompt, return url of image, base64 data and error
func (c *ChatInstance) CreateImageRequest(props ImageProps) (string, string, error) {
res, err := utils.Post(
c.GetImageEndpoint(),
c.GetHeader(), ImageRequest{
Model: props.Model,
Prompt: props.Prompt,
Size: utils.Multi[ImageSize](
props.Model == globals.Dalle3 || props.Model == globals.GPTImage1,
ImageSize1024,
ImageSize512,
),
N: 1,
}, props.Proxy)
if err != nil || res == nil {
return "", "", fmt.Errorf(err.Error())
}
data := utils.MapToStruct[ImageResponse](res)
if data == nil {
return "", "", fmt.Errorf("openai error: cannot parse response")
} else if data.Error.Message != "" {
return "", "", fmt.Errorf(data.Error.Message)
}
// for gpt-image-1, return base64 data if available
if props.Model == globals.GPTImage1 && data.Data[0].B64Json != "" {
return "", data.Data[0].B64Json, nil
}
return data.Data[0].Url, "", nil
}
// CreateImage will create a dalle image from prompt, return markdown of image
func (c *ChatInstance) CreateImage(props *adaptercommon.ChatProps) (string, error) {
url, b64Json, err := c.CreateImageRequest(ImageProps{
Model: props.Model,
Prompt: c.GetLatestPrompt(props),
Proxy: props.Proxy,
})
if err != nil {
if strings.Contains(err.Error(), "safety") {
return err.Error(), nil
}
return "", err
}
if b64Json != "" {
return utils.GetBase64ImageMarkdown(b64Json), nil
}
storedUrl := utils.StoreImage(url)
return utils.GetImageMarkdown(storedUrl), nil
}
================================================
FILE: adapter/openai/processor.go
================================================
package openai
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"errors"
"fmt"
"regexp"
)
func formatMessages(props *adaptercommon.ChatProps) interface{} {
if globals.IsVisionModel(props.Model) {
return utils.Each[globals.Message, Message](props.Message, func(message globals.Message) Message {
if message.Role == globals.User {
content, urls := utils.ExtractImages(message.Content, true)
images := utils.EachNotNil[string, MessageContent](urls, func(url string) *MessageContent {
obj, err := utils.NewImage(url)
props.Buffer.AddImage(obj)
if err != nil {
globals.Info(fmt.Sprintf("cannot process image: %s (source: %s)", err.Error(), utils.Extract(url, 24, "...")))
}
return &MessageContent{
Type: "image_url",
ImageUrl: &ImageUrl{
Url: url,
},
}
})
return Message{
Role: message.Role,
Content: utils.Prepend(images, MessageContent{
Type: "text",
Text: &content,
}),
Name: message.Name,
FunctionCall: message.FunctionCall,
ToolCalls: message.ToolCalls,
ToolCallId: message.ToolCallId,
}
}
return Message{
Role: message.Role,
Content: MessageContents{
MessageContent{
Type: "text",
Text: &message.Content,
},
},
Name: message.Name,
FunctionCall: message.FunctionCall,
ToolCalls: message.ToolCalls,
ToolCallId: message.ToolCallId,
}
})
}
return props.Message
}
func processChatResponse(data string) *ChatStreamResponse {
return utils.UnmarshalForm[ChatStreamResponse](data)
}
func processCompletionResponse(data string) *CompletionResponse {
return utils.UnmarshalForm[CompletionResponse](data)
}
func processChatErrorResponse(data string) *ChatStreamErrorResponse {
return utils.UnmarshalForm[ChatStreamErrorResponse](data)
}
func getChoices(form *ChatStreamResponse) *globals.Chunk {
if len(form.Choices) == 0 {
return &globals.Chunk{Content: ""}
}
choice := form.Choices[0].Delta
return &globals.Chunk{
Content: choice.Content,
ToolCall: choice.ToolCalls,
FunctionCall: choice.FunctionCall,
}
}
func getCompletionChoices(form *CompletionResponse) string {
if len(form.Choices) == 0 {
return ""
}
return form.Choices[0].Text
}
func getRobustnessResult(chunk string) string {
exp := `\"content\":\"(.*?)\"`
compile, err := regexp.Compile(exp)
if err != nil {
return ""
}
matches := compile.FindStringSubmatch(chunk)
if len(matches) > 1 {
return utils.ProcessRobustnessChar(matches[1])
} else {
return ""
}
}
func (c *ChatInstance) ProcessLine(data string, isCompletionType bool) (*globals.Chunk, error) {
if isCompletionType {
// openai legacy support
if completion := processCompletionResponse(data); completion != nil {
return &globals.Chunk{
Content: getCompletionChoices(completion),
}, nil
}
globals.Warn(fmt.Sprintf("openai error: cannot parse completion response: %s", data))
return &globals.Chunk{Content: ""}, errors.New("parser error: cannot parse completion response")
}
if form := processChatResponse(data); form != nil {
return getChoices(form), nil
}
if form := processChatErrorResponse(data); form != nil {
return &globals.Chunk{Content: ""}, errors.New(fmt.Sprintf("openai error: %s (type: %s)", form.Error.Message, form.Error.Type))
}
globals.Warn(fmt.Sprintf("openai error: cannot parse chat completion response: %s", data))
return &globals.Chunk{Content: ""}, errors.New("parser error: cannot parse chat completion response")
}
================================================
FILE: adapter/openai/struct.go
================================================
package openai
import (
factory "chat/adapter/common"
"chat/globals"
"fmt"
)
type ChatInstance struct {
Endpoint string
ApiKey string
}
func (c *ChatInstance) GetEndpoint() string {
return c.Endpoint
}
func (c *ChatInstance) GetApiKey() string {
return c.ApiKey
}
func (c *ChatInstance) GetHeader() map[string]string {
return map[string]string{
"Content-Type": "application/json",
"Authorization": fmt.Sprintf("Bearer %s", c.GetApiKey()),
}
}
func NewChatInstance(endpoint, apiKey string) *ChatInstance {
return &ChatInstance{
Endpoint: endpoint,
ApiKey: apiKey,
}
}
func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {
return NewChatInstance(
conf.GetEndpoint(),
conf.GetRandomSecret(),
)
}
================================================
FILE: adapter/openai/types.go
================================================
package openai
import "chat/globals"
type ImageUrl struct {
Url string `json:"url"`
Detail *string `json:"detail,omitempty"`
}
type MessageContent struct {
Type string `json:"type"`
Text *string `json:"text,omitempty"`
ImageUrl *ImageUrl `json:"image_url,omitempty"`
}
type MessageContents []MessageContent
type Message struct {
Role string `json:"role"`
Content MessageContents `json:"content"`
Name *string `json:"name,omitempty"`
FunctionCall *globals.FunctionCall `json:"function_call,omitempty"` // only `function` role
ToolCallId *string `json:"tool_call_id,omitempty"` // only `tool` role
ToolCalls *globals.ToolCalls `json:"tool_calls,omitempty"` // only `assistant` role
ReasoningContent *string `json:"reasoning,omitempty"` // only for claude reasoning models
}
// ChatRequest is the request body for openai
type ChatRequest struct {
Model string `json:"model"`
Messages interface{} `json:"messages"`
MaxToken *int `json:"max_tokens,omitempty"`
MaxCompletionTokens *int `json:"max_completion_tokens,omitempty"`
Stream bool `json:"stream"`
PresencePenalty *float32 `json:"presence_penalty,omitempty"`
FrequencyPenalty *float32 `json:"frequency_penalty,omitempty"`
Temperature *float32 `json:"temperature,omitempty"`
TopP *float32 `json:"top_p,omitempty"`
Tools *globals.FunctionTools `json:"tools,omitempty"`
ToolChoice *interface{} `json:"tool_choice,omitempty"` // string or object
}
// CompletionRequest is the request body for openai completion
type CompletionRequest struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
MaxToken *int `json:"max_tokens,omitempty"`
Stream bool `json:"stream"`
}
// ChatResponse is the native http request body for openai
type ChatResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []struct {
Index int `json:"index"`
Message globals.Message `json:"message"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
Error struct {
Message string `json:"message"`
} `json:"error"`
}
// ChatStreamResponse is the stream response body for openai
type ChatStreamResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []struct {
Delta globals.Message `json:"delta"`
Index int `json:"index"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
}
// CompletionResponse is the native http request body / stream response body for openai completion
type CompletionResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []struct {
Text string `json:"text"`
Index int `json:"index"`
} `json:"choices"`
}
type ChatStreamErrorResponse struct {
Error struct {
Message string `json:"message"`
Type string `json:"type"`
} `json:"error"`
}
type ImageSize string
// ImageRequest is the request body for openai dalle image generation
type ImageRequest struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
Size ImageSize `json:"size"`
N int `json:"n"`
}
type ImageResponse struct {
Data []struct {
Url string `json:"url,omitempty"`
B64Json string `json:"b64_json,omitempty"`
} `json:"data"`
Error struct {
Message string `json:"message"`
} `json:"error"`
}
var (
ImageSize256 ImageSize = "256x256"
ImageSize512 ImageSize = "512x512"
ImageSize1024 ImageSize = "1024x1024"
)
================================================
FILE: adapter/openai/videos.go
================================================
package openai
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"fmt"
"time"
)
type VideoRequest struct {
Prompt string `json:"prompt"`
Model string `json:"model,omitempty"`
Seconds *string `json:"seconds,omitempty"`
Size *string `json:"size,omitempty"`
InputReference *string `json:"input_reference,omitempty"`
}
type VideoJob struct {
CompletedAt *int64 `json:"completed_at,omitempty"`
CreatedAt int64 `json:"created_at"`
ExpiresAt *int64 `json:"expires_at,omitempty"`
Id string `json:"id"`
Model string `json:"model"`
Object string `json:"object"`
Progress *int `json:"progress,omitempty"`
Prompt string `json:"prompt"`
RemixedFromVideoId *string `json:"remixed_from_video_id,omitempty"`
Seconds string `json:"seconds"`
Size string `json:"size"`
Status string `json:"status"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error,omitempty"`
}
func (c *ChatInstance) getVideoCreateEndpoint() string {
return fmt.Sprintf("%s/v1/videos", c.GetEndpoint())
}
func (c *ChatInstance) getVideoQueryEndpoint(id string) string {
return fmt.Sprintf("%s/v1/videos/%s", c.GetEndpoint(), id)
}
func (c *ChatInstance) CreateVideoRequest(props *adaptercommon.VideoProps, hook globals.Hook) error {
body := VideoRequest{
Prompt: props.Prompt,
Model: props.Model,
Seconds: props.Seconds,
Size: props.Size,
InputReference: props.InputReference,
}
res, err := utils.Post(c.getVideoCreateEndpoint(), c.GetHeader(), body, props.Proxy)
if err != nil || res == nil {
if err != nil {
return fmt.Errorf("openai video error: %s", err.Error())
}
return fmt.Errorf("openai video error: empty response")
}
job := utils.MapToStruct[VideoJob](res)
if job == nil {
return fmt.Errorf("openai video error: cannot parse response")
}
if job.Error != nil && (job.Error.Message != "") {
return fmt.Errorf("openai video error: %s", job.Error.Message)
}
const maxTimeout = 30 * time.Minute
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
deadline := time.After(maxTimeout)
var begin bool
var lastProgress int = -1
for {
if job.Status == "completed" {
if begin {
if err := hook(&globals.Chunk{Content: "```\n"}); err != nil {
return err
}
}
return hook(&globals.Chunk{Content: utils.Marshal(job)})
}
if job.Status == "failed" {
if begin {
if err := hook(&globals.Chunk{Content: "```\n"}); err != nil {
return err
}
}
if job.Error != nil && job.Error.Message != "" {
return fmt.Errorf("openai video job failed: %s", job.Error.Message)
}
return fmt.Errorf("openai video job failed")
}
select {
case <-ticker.C:
if job.Id == "" {
return hook(&globals.Chunk{Content: utils.Marshal(job)})
}
data, gErr := utils.Get(c.getVideoQueryEndpoint(job.Id), c.GetHeader(), props.Proxy)
if gErr != nil || data == nil {
continue
}
if j := utils.MapToStruct[VideoJob](data); j != nil {
job = j
}
progress := 0
if job.Progress != nil {
progress = *job.Progress
}
if !begin {
begin = true
if err := hook(&globals.Chunk{Content: "```progress\n"}); err != nil {
return err
}
}
if progress != lastProgress {
if err := hook(&globals.Chunk{Content: fmt.Sprintf("%d\n", progress)}); err != nil {
return err
}
lastProgress = progress
}
case <-deadline:
if begin {
if err := hook(&globals.Chunk{Content: "```\n"}); err != nil {
return err
}
}
return fmt.Errorf("openai video job timeout")
}
}
}
================================================
FILE: adapter/palm2/chat.go
================================================
package palm2
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"errors"
"fmt"
"strings"
)
var geminiMaxImages = 16
func (c *ChatInstance) GetChatEndpoint(model string, stream bool) string {
if model == globals.ChatBison001 {
return fmt.Sprintf("%s/v1beta2/models/%s:generateMessage?key=%s", c.Endpoint, model, c.ApiKey)
}
if stream {
return fmt.Sprintf("%s/v1beta/models/%s:streamGenerateContent?alt=sse&key=%s", c.Endpoint, model, c.ApiKey)
}
return fmt.Sprintf("%s/v1beta/models/%s:generateContent?key=%s", c.Endpoint, model, c.ApiKey)
}
func (c *ChatInstance) ConvertMessage(message []globals.Message) []PalmMessage {
var result []PalmMessage
for i, item := range message {
if len(item.Content) == 0 {
// palm model: message must include non empty content
continue
}
if item.Role == globals.Tool {
continue
}
if i > 0 && item.Role == result[len(result)-1].Author {
// palm model: messages must alternate between authors
result[len(result)-1].Content += " " + item.Content
continue
}
result = append(result, PalmMessage{
Author: item.Role,
Content: item.Content,
})
}
return result
}
func (c *ChatInstance) GetPalm2ChatBody(props *adaptercommon.ChatProps) *PalmChatBody {
return &PalmChatBody{
Prompt: PalmPrompt{
Messages: c.ConvertMessage(props.Message),
},
}
}
func (c *ChatInstance) GetGeminiChatBody(props *adaptercommon.ChatProps) *GeminiChatBody {
return &GeminiChatBody{
Contents: c.GetGeminiContents(props.Model, props.Message),
GenerationConfig: GeminiConfig{
Temperature: props.Temperature,
MaxOutputTokens: props.MaxTokens,
TopP: props.TopP,
TopK: props.TopK,
},
}
}
func (c *ChatInstance) GetPalm2ChatResponse(data interface{}) (string, error) {
if form := utils.MapToStruct[PalmChatResponse](data); form != nil {
if len(form.Candidates) == 0 {
return "", fmt.Errorf("palm2 error: the content violates content policy")
}
return form.Candidates[0].Content, nil
}
return "", fmt.Errorf("palm2 error: cannot parse response")
}
func (c *ChatInstance) GetGeminiChatResponse(data interface{}) (string, error) {
if form := utils.MapToStruct[GeminiChatResponse](data); form != nil {
if len(form.Candidates) != 0 && len(form.Candidates[0].Content.Parts) != 0 {
return form.Candidates[0].Content.Parts[0].Text, nil
}
}
if form := utils.MapToStruct[GeminiChatErrorResponse](data); form != nil {
return "", fmt.Errorf("gemini error: %s (code: %d, status: %s)", form.Error.Message, form.Error.Code, form.Error.Status)
}
return "", fmt.Errorf("gemini: cannot parse response")
}
func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {
uri := c.GetChatEndpoint(props.Model, false)
if props.Model == globals.ChatBison001 {
data, err := utils.Post(uri, map[string]string{
"Content-Type": "application/json",
}, c.GetPalm2ChatBody(props), props.Proxy)
if err != nil {
return "", fmt.Errorf("palm2 error: %s", err.Error())
}
return c.GetPalm2ChatResponse(data)
}
data, err := utils.Post(uri, map[string]string{
"Content-Type": "application/json",
}, c.GetGeminiChatBody(props), props.Proxy)
if err != nil {
return "", fmt.Errorf("gemini error: %s", err.Error())
}
return c.GetGeminiChatResponse(data)
}
// CreateStreamChatRequest is the stream request for palm2
func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {
// Handle imagen models
if globals.IsGoogleImagenModel(props.Model) {
response, err := c.CreateImage(props)
if err != nil {
return err
}
return callback(&globals.Chunk{Content: response})
}
// Handle chat models
if props.Model == globals.ChatBison001 {
response, err := c.CreateChatRequest(props)
if err != nil {
return err
}
for _, item := range utils.SplitItem(response, " ") {
if err := callback(&globals.Chunk{Content: item}); err != nil {
return err
}
}
return nil
}
ticks := 0
scanErr := utils.EventScanner(&utils.EventScannerProps{
Method: "POST",
Uri: c.GetChatEndpoint(props.Model, true),
Headers: map[string]string{
"Content-Type": "application/json",
},
Body: c.GetGeminiChatBody(props),
Callback: func(data string) error {
ticks += 1
if form := utils.UnmarshalForm[GeminiStreamResponse](data); form != nil {
if len(form.Candidates) != 0 && len(form.Candidates[0].Content.Parts) != 0 {
return callback(&globals.Chunk{
Content: form.Candidates[0].Content.Parts[0].Text,
})
}
return nil
}
if form := utils.UnmarshalForm[GeminiChatErrorResponse](data); form != nil {
return fmt.Errorf("gemini error: %s (code: %d, status: %s)", form.Error.Message, form.Error.Code, form.Error.Status)
}
return nil
},
}, props.Proxy)
if scanErr != nil {
if scanErr.Error != nil && strings.Contains(scanErr.Error.Error(), "status code: 404") {
// downgrade to non-stream request
response, err := c.CreateChatRequest(props)
if err != nil {
return err
}
return callback(&globals.Chunk{Content: response})
}
if scanErr.Body != "" {
if form := utils.UnmarshalForm[GeminiChatErrorResponse](scanErr.Body); form != nil {
return fmt.Errorf("gemini error: %s (code: %d, status: %s)", form.Error.Message, form.Error.Code, form.Error.Status)
}
return fmt.Errorf("gemini error: %s", scanErr.Body)
}
return fmt.Errorf("gemini error: %v", scanErr.Error)
}
if ticks == 0 {
return errors.New("no response")
}
return nil
}
func (c *ChatInstance) GetLatestPrompt(props *adaptercommon.ChatProps) string {
if len(props.Message) == 0 {
return ""
}
return props.Message[len(props.Message)-1].Content
}
================================================
FILE: adapter/palm2/formatter.go
================================================
package palm2
import (
"chat/globals"
"chat/utils"
"strings"
)
func getGeminiRole(role string) string {
switch role {
case globals.User:
return GeminiUserType
case globals.Assistant, globals.Tool, globals.System:
return GeminiModelType
default:
return GeminiUserType
}
}
func getMimeType(content string) string {
segment := strings.Split(content, ".")
if len(segment) == 0 || len(segment) == 1 {
return "image/png"
}
suffix := strings.TrimSpace(strings.ToLower(segment[len(segment)-1]))
switch suffix {
case "png":
return "image/png"
case "jpg", "jpeg":
return "image/jpeg"
case "gif":
return "image/gif"
case "webp":
return "image/webp"
case "heif":
return "image/heif"
case "heic":
return "image/heic"
default:
return "image/png"
}
}
func getGeminiContent(parts []GeminiChatPart, content string, model string) []GeminiChatPart {
if model == globals.GeminiPro {
return append(parts, GeminiChatPart{
Text: &content,
})
}
raw, urls := utils.ExtractImages(content, true)
if len(urls) > geminiMaxImages {
urls = urls[:geminiMaxImages]
}
parts = append(parts, GeminiChatPart{
Text: &raw,
})
for _, url := range urls {
data, err := utils.ConvertToBase64(url)
if err != nil {
continue
}
parts = append(parts, GeminiChatPart{
InlineData: &GeminiInlineData{
MimeType: getMimeType(url),
Data: data,
},
})
}
return parts
}
func (c *ChatInstance) GetGeminiContents(model string, message []globals.Message) []GeminiContent {
// gemini role should be user-model
result := make([]GeminiContent, 0)
for _, item := range message {
role := getGeminiRole(item.Role)
if len(item.Content) == 0 {
// gemini model: message must include non empty content
continue
}
if len(result) == 0 && getGeminiRole(item.Role) == GeminiModelType {
// gemini model: first message must be user
result = append(result, GeminiContent{
Role: GeminiUserType,
Parts: getGeminiContent(make([]GeminiChatPart, 0), "", model),
})
}
if len(result) > 0 && role == result[len(result)-1].Role {
// gemini model: messages must alternate between authors
result[len(result)-1].Parts = getGeminiContent(result[len(result)-1].Parts, item.Content, model)
continue
}
result = append(result, GeminiContent{
Role: getGeminiRole(item.Role),
Parts: getGeminiContent(make([]GeminiChatPart, 0), item.Content, model),
})
}
return result
}
================================================
FILE: adapter/palm2/image.go
================================================
package palm2
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"fmt"
"strings"
)
type ImageProps struct {
Model string
Prompt string
Proxy globals.ProxyConfig
}
func (c *ChatInstance) GetImageEndpoint(model string) string {
return fmt.Sprintf("%s/v1beta/models/%s:predict?key=%s", c.Endpoint, model, c.ApiKey)
}
// CreateImageRequest will create a gemini imagen from prompt, return base64 of image and error
func (c *ChatInstance) CreateImageRequest(props ImageProps) (string, error) {
res, err := utils.Post(
c.GetImageEndpoint(props.Model),
map[string]string{
"Content-Type": "application/json",
},
ImageRequest{
Instances: []ImageInstance{
{
Prompt: props.Prompt,
},
},
Parameters: ImageParameters{
SampleCount: 1,
AspectRatio: "1:1",
PersonGeneration: "allow_adult",
},
},
props.Proxy,
)
if err != nil || res == nil {
return "", fmt.Errorf("gemini error: %s", err.Error())
}
data := utils.MapToStruct[ImageResponse](res)
if data == nil {
return "", fmt.Errorf("gemini error: cannot parse response")
}
if len(data.Predictions) == 0 {
return "", fmt.Errorf("gemini error: no image generated")
}
return data.Predictions[0].BytesBase64Encoded, nil
}
// CreateImage will create a gemini imagen from prompt, return markdown of image
func (c *ChatInstance) CreateImage(props *adaptercommon.ChatProps) (string, error) {
if !globals.IsGoogleImagenModel(props.Model) {
return "", nil
}
base64Data, err := c.CreateImageRequest(ImageProps{
Model: props.Model,
Prompt: c.GetLatestPrompt(props),
Proxy: props.Proxy,
})
if err != nil {
if strings.Contains(err.Error(), "safety") {
return err.Error(), nil
}
return "", err
}
// Convert base64 to data URL format
dataUrl := fmt.Sprintf("data:image/png;base64,%s", base64Data)
url := utils.StoreImage(dataUrl)
return utils.GetImageMarkdown(url), nil
}
================================================
FILE: adapter/palm2/struct.go
================================================
package palm2
import (
factory "chat/adapter/common"
"chat/globals"
)
type ChatInstance struct {
Endpoint string
ApiKey string
}
func (c *ChatInstance) GetApiKey() string {
return c.ApiKey
}
func (c *ChatInstance) GetEndpoint() string {
return c.Endpoint
}
func NewChatInstance(endpoint string, apiKey string) *ChatInstance {
return &ChatInstance{
Endpoint: endpoint,
ApiKey: apiKey,
}
}
func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {
return NewChatInstance(
conf.GetEndpoint(),
conf.GetRandomSecret(),
)
}
================================================
FILE: adapter/palm2/types.go
================================================
package palm2
const (
GeminiUserType = "user"
GeminiModelType = "model"
)
type PalmMessage struct {
Author string `json:"author"`
Content string `json:"content"`
}
// PalmChatBody is the native http request body for palm2
type PalmChatBody struct {
Prompt PalmPrompt `json:"prompt"`
}
type PalmPrompt struct {
Messages []PalmMessage `json:"messages"`
}
// PalmChatResponse is the native http response body for palm2
type PalmChatResponse struct {
Candidates []PalmMessage `json:"candidates"`
}
// GeminiChatBody is the native http request body for gemini
type GeminiChatBody struct {
Contents []GeminiContent `json:"contents"`
GenerationConfig GeminiConfig `json:"generationConfig"`
}
type GeminiConfig struct {
Temperature *float32 `json:"temperature,omitempty"`
MaxOutputTokens *int `json:"maxOutputTokens,omitempty"`
TopP *float32 `json:"topP,omitempty"`
TopK *int `json:"topK,omitempty"`
}
type GeminiContent struct {
Role string `json:"role"`
Parts []GeminiChatPart `json:"parts"`
}
type GeminiChatPart struct {
Text *string `json:"text,omitempty"`
InlineData *GeminiInlineData `json:"inline_data,omitempty"`
}
type GeminiInlineData struct {
MimeType string `json:"mime_type"`
Data string `json:"data"`
}
type GeminiChatResponse struct {
Candidates []struct {
Content struct {
Parts []struct {
Text string `json:"text"`
} `json:"parts"`
Role string `json:"role"`
} `json:"content"`
} `json:"candidates"`
}
type GeminiChatErrorResponse struct {
Error struct {
Code int `json:"code"`
Message string `json:"message"`
Status string `json:"status"`
} `json:"error"`
}
type GeminiStreamResponse struct {
Candidates []struct {
Content struct {
Parts []struct {
Text string `json:"text"`
} `json:"parts"`
Role string `json:"role"`
} `json:"content"`
} `json:"candidates"`
}
// ImageRequest is the native http request body for imagen
type ImageRequest struct {
Instances []ImageInstance `json:"instances"`
Parameters ImageParameters `json:"parameters"`
}
type ImageInstance struct {
Prompt string `json:"prompt"`
}
type ImageParameters struct {
SampleCount int `json:"sampleCount,omitempty"`
AspectRatio string `json:"aspectRatio,omitempty"`
PersonGeneration string `json:"personGeneration,omitempty"`
}
// ImageResponse is the native http response body for imagen
type ImageResponse struct {
Predictions []ImagePrediction `json:"predictions"`
}
type ImagePrediction struct {
MimeType string `json:"mimeType"`
BytesBase64Encoded string `json:"bytesBase64Encoded"`
}
================================================
FILE: adapter/request.go
================================================
package adapter
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"fmt"
"strings"
"time"
)
func IsAvailableError(err error) bool {
return err != nil && (err.Error() != "signal" && !strings.Contains(err.Error(), "signal"))
}
func IsSkipError(err error) bool {
return err == nil || (err.Error() == "signal" || strings.Contains(err.Error(), "signal"))
}
func isQPSOverLimit(model string, err error) bool {
if strings.Contains(model, "spark-desk") {
return strings.Contains(err.Error(), "AppIdQpsOverFlowError")
}
return false
}
func NewChatRequest(conf globals.ChannelConfig, props *adaptercommon.ChatProps, hook globals.Hook) error {
err := createChatRequest(conf, props, hook)
retries := conf.GetRetry()
props.Current++
if IsAvailableError(err) {
if isQPSOverLimit(props.OriginalModel, err) {
// sleep for 0.5s to avoid qps limit
globals.Info(fmt.Sprintf("qps limit for %s, sleep and retry (times: %d)", props.OriginalModel, props.Current))
time.Sleep(500 * time.Millisecond)
return NewChatRequest(conf, props, hook)
}
if props.Current < retries {
content := strings.Replace(err.Error(), "\n", "", -1)
globals.Warn(fmt.Sprintf("retrying chat request for %s (attempt %d/%d, error: %s)", props.OriginalModel, props.Current+1, retries, content))
return NewChatRequest(conf, props, hook)
}
}
return conf.ProcessError(err)
}
func NewVideoRequest(conf globals.ChannelConfig, props *adaptercommon.VideoProps, hook globals.Hook) error {
err := createVideoRequest(conf, props, hook)
retries := conf.GetRetry()
props.Current++
if IsAvailableError(err) {
if isQPSOverLimit(props.OriginalModel, err) {
// sleep for 0.5s to avoid qps limit
globals.Info(fmt.Sprintf("qps limit for %s, sleep and retry (times: %d)", props.OriginalModel, props.Current))
time.Sleep(500 * time.Millisecond)
return NewVideoRequest(conf, props, hook)
}
if props.Current < retries {
content := strings.Replace(err.Error(), "\n", "", -1)
globals.Info(fmt.Sprintf("retrying error request for %s (attempt %d/%d, error: %s)", props.OriginalModel, props.Current+1, retries, content))
return NewVideoRequest(conf, props, hook)
}
}
return conf.ProcessError(err)
}
func ClearMessages(model string, messages []globals.Message) []globals.Message {
if globals.IsVisionModel(model) {
return messages
}
return utils.Each[globals.Message](messages, func(message globals.Message) globals.Message {
if message.Role != globals.User {
return message
}
images := utils.ExtractBase64Images(message.Content)
for _, image := range images {
if len(image) <= 46 {
continue
}
message.Content = strings.Replace(message.Content, image, utils.Extract(image, 46, " ..."), -1)
}
return message
})
}
================================================
FILE: adapter/router.go
================================================
package adapter
import (
"chat/adapter/midjourney"
"github.com/gin-gonic/gin"
)
func Register(app *gin.RouterGroup) {
app.POST("/mj/notify", midjourney.NotifyAPI)
}
================================================
FILE: adapter/skylark/chat.go
================================================
package skylark
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"context"
"fmt"
"io"
"github.com/volcengine/volcengine-go-sdk/service/arkruntime/model"
"github.com/volcengine/volcengine-go-sdk/volcengine"
)
const defaultMaxTokens int = 4096
func getMessages(messages []globals.Message) []*model.ChatCompletionMessage {
result := make([]*model.ChatCompletionMessage, 0)
for _, message := range messages {
if message.Role == globals.Tool {
message.Role = model.ChatMessageRoleTool
}
msg := &model.ChatCompletionMessage{
Role: message.Role,
Content: &model.ChatCompletionMessageContent{StringValue: volcengine.String(message.Content)},
FunctionCall: getFunctionCall(message.ToolCalls),
ReasoningContent: message.ReasoningContent,
}
hasPrevious := len(result) > 0
// a message should not followed by the same role message, merge them
if hasPrevious && result[len(result)-1].Role == message.Role {
prev := result[len(result)-1]
prev.Content.StringValue = volcengine.String(*prev.Content.StringValue + *msg.Content.StringValue)
if message.ToolCalls != nil {
prev.FunctionCall = msg.FunctionCall
}
continue
}
// `assistant` message should follow a user or function message, if not has previous message, change the role to `user`
if !hasPrevious && message.Role == model.ChatMessageRoleAssistant {
msg.Role = model.ChatMessageRoleUser
}
result = append(result, msg)
}
return result
}
func (c *ChatInstance) GetMaxTokens(token *int) int {
if token == nil || *token < 0 {
return defaultMaxTokens
}
return *token
}
func (c *ChatInstance) CreateRequest(props *adaptercommon.ChatProps) *model.ChatCompletionRequest {
return &model.ChatCompletionRequest{
Model: props.Model,
Messages: getMessages(props.Message),
Temperature: utils.GetPtrVal(props.Temperature, 0.),
TopP: utils.GetPtrVal(props.TopP, 0.),
// skylark v3 not support TopK
PresencePenalty: utils.GetPtrVal(props.PresencePenalty, 0.),
FrequencyPenalty: utils.GetPtrVal(props.FrequencyPenalty, 0.),
RepetitionPenalty: utils.GetPtrVal(props.RepetitionPenalty, 0.),
MaxTokens: c.GetMaxTokens(props.MaxTokens),
FunctionCall: getFunctions(props.Tools),
}
}
func getToolCalls(id string, choiceDelta model.ChatCompletionStreamChoiceDelta) *globals.ToolCalls {
calls := choiceDelta.FunctionCall
if calls == nil {
return nil
}
return &globals.ToolCalls{
globals.ToolCall{
Type: "function",
Id: fmt.Sprintf("%s-%s", calls.Name, id),
Function: globals.ToolCallFunction{
Name: calls.Name,
Arguments: calls.Arguments,
},
},
}
}
func getChoice(choice model.ChatCompletionStreamResponse) *globals.Chunk {
if len(choice.Choices) == 0 {
return &globals.Chunk{Content: ""}
}
message := choice.Choices[0].Delta
return &globals.Chunk{
Content: message.Content,
ToolCall: getToolCalls(choice.ID, message),
}
}
func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {
req := c.CreateRequest(props)
c.isFirstReasoning = true
c.isReasonOver = false
if globals.DebugMode {
globals.Debug(fmt.Sprintf("[skylark] request: %v", utils.Marshal(req)))
}
stream, err := c.Instance.CreateChatCompletionStream(context.Background(), req)
if err != nil {
if globals.DebugMode {
globals.Debug(fmt.Sprintf("[skylark] stream error: %v", err))
}
return err
}
defer stream.Close()
for {
recv, err2 := stream.Recv()
if err2 == io.EOF {
return nil
}
if err2 != nil {
if globals.DebugMode {
globals.Debug(fmt.Sprintf("[skylark] receive error: %v", err2))
}
return err2
}
if globals.DebugMode {
globals.Debug(fmt.Sprintf("[skylark] response: %v", utils.Marshal(recv)))
}
choice := getChoice(recv)
if len(recv.Choices) > 0 {
delta := recv.Choices[0].Delta
// Handle reasoning content
if c.isFirstReasoning == false && !c.isReasonOver && delta.ReasoningContent == nil {
c.isReasonOver = true
if delta.Content != "" {
choice.Content = fmt.Sprintf("\n \n\n%s", delta.Content)
} else {
choice.Content = "\n\n\n"
}
}
if delta.ReasoningContent != nil {
if c.isFirstReasoning {
c.isFirstReasoning = false
choice.Content = fmt.Sprintf("\n%s", *delta.ReasoningContent)
} else {
choice.Content = *delta.ReasoningContent
}
}
}
if err = callback(choice); err != nil {
return err
}
}
return nil
}
================================================
FILE: adapter/skylark/formatter.go
================================================
package skylark
import (
"chat/globals"
"chat/utils"
"github.com/volcengine/volcengine-go-sdk/service/arkruntime/model"
structpb "github.com/golang/protobuf/ptypes/struct"
"github.com/volcengine/volc-sdk-golang/service/maas/models/api"
)
func getFunctionCall(calls *globals.ToolCalls) *model.FunctionCall {
if calls == nil || len(*calls) == 0 {
return nil
}
call := (*calls)[0]
return &model.FunctionCall{
Name: call.Function.Name,
Arguments: call.Function.Arguments,
}
}
func getType(p globals.ToolProperty) string {
t, ok := p["type"]
if !ok {
return "string"
}
return t.(string)
}
func getDescription(p globals.ToolProperty) string {
desc, ok := p["description"]
if !ok {
return ""
}
return desc.(string)
}
func getValue(p globals.ToolProperty) *structpb.Value {
switch getType(p) {
case "string", "enum":
return &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: getDescription(p)}}
case "number":
return &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: 0}}
case "boolean":
return &structpb.Value{Kind: &structpb.Value_BoolValue{BoolValue: false}}
case "object":
return &structpb.Value{Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{Fields: map[string]*structpb.Value{}}}}
case "array":
return &structpb.Value{Kind: &structpb.Value_ListValue{ListValue: &structpb.ListValue{Values: []*structpb.Value{}}}}
default:
return nil
}
}
func getFunctions(tools *globals.FunctionTools) []*api.Function {
if tools == nil || len(*tools) == 0 {
return nil
}
return utils.Each[globals.ToolObject, *api.Function](*tools, func(tool globals.ToolObject) *api.Function {
param := &structpb.Struct{
Fields: map[string]*structpb.Value{},
}
for k, v := range tool.Function.Parameters.Properties {
param.Fields[k] = getValue(v)
}
return &api.Function{
Name: tool.Function.Name,
Description: tool.Function.Description,
Parameters: param,
}
})
}
================================================
FILE: adapter/skylark/struct.go
================================================
package skylark
import (
factory "chat/adapter/common"
"chat/globals"
"github.com/volcengine/volcengine-go-sdk/service/arkruntime"
)
type ChatInstance struct {
Instance *arkruntime.Client
isFirstReasoning bool
isReasonOver bool
}
func NewChatInstance(endpoint, apiKey string) *ChatInstance {
//https://ark.cn-beijing.volces.com/api/v3
instance := arkruntime.NewClientWithApiKey(apiKey, arkruntime.WithBaseUrl(endpoint))
return &ChatInstance{
Instance: instance,
isFirstReasoning: true,
isReasonOver: false,
}
}
func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {
params := conf.SplitRandomSecret(1)
return NewChatInstance(
conf.GetEndpoint(),
params[0],
)
}
================================================
FILE: adapter/slack/chat.go
================================================
package slack
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"context"
)
func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, hook globals.Hook) error {
if err := c.Instance.NewChannel(c.GetChannel()); err != nil {
return err
}
resp, err := c.Instance.Reply(context.Background(), c.FormatMessage(props.Message), nil)
if err != nil {
return err
}
return c.ProcessPartialResponse(resp, hook)
}
================================================
FILE: adapter/slack/struct.go
================================================
package slack
import (
factory "chat/adapter/common"
"chat/globals"
"fmt"
"github.com/bincooo/claude-api"
"github.com/bincooo/claude-api/types"
"github.com/bincooo/claude-api/vars"
"strings"
)
type ChatInstance struct {
BotId string
Token string
Channel string
Instance types.Chat
}
func (c *ChatInstance) GetBotId() string {
return c.BotId
}
func (c *ChatInstance) GetToken() string {
return c.Token
}
func (c *ChatInstance) GetChannel() string {
return c.Channel
}
func (c *ChatInstance) GetInstance() types.Chat {
return c.Instance
}
func NewChatInstance(botId, token, channel string) *ChatInstance {
options := claude.NewDefaultOptions(token, botId, vars.Model4Slack)
if instance, err := claude.New(options); err != nil {
return nil
} else {
return &ChatInstance{
BotId: botId,
Token: token,
Channel: channel,
Instance: instance,
}
}
}
func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {
params := conf.SplitRandomSecret(2)
return NewChatInstance(
params[0], params[1],
conf.GetEndpoint(),
)
}
func (c *ChatInstance) FormatMessage(message []globals.Message) string {
result := make([]string, len(message))
for i, item := range message {
if item.Role == globals.Tool {
continue
}
result[i] = fmt.Sprintf("%s: %s", item.Role, item.Content)
}
return strings.Join(result, "\n\n")
}
func (c *ChatInstance) ProcessPartialResponse(res chan types.PartialResponse, hook globals.Hook) error {
for {
select {
case data, ok := <-res:
if !ok {
return nil
}
if data.Error != nil {
return data.Error
} else if data.Text != "" {
if err := hook(&globals.Chunk{Content: data.Text}); err != nil {
return err
}
}
}
}
}
================================================
FILE: adapter/sparkdesk/chat.go
================================================
package sparkdesk
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"fmt"
"strings"
)
var FunctionCallingModels = []string{
globals.SparkDeskMax,
globals.SparkDeskV4Ultra,
}
func GetToken(props *adaptercommon.ChatProps) *int {
if props.MaxTokens == nil {
return nil
}
switch props.Model {
case globals.SparkDeskLite, globals.SparkDeskPro128K:
if *props.MaxTokens > 4096 {
return utils.ToPtr(4096)
}
case globals.SparkDeskPro, globals.SparkDeskMax, globals.SparkDeskMax32K, globals.SparkDeskV4Ultra:
if *props.MaxTokens > 8192 {
return utils.ToPtr(8192)
}
}
return props.MaxTokens
}
func GetTopK(props *adaptercommon.ChatProps) *int {
if props.TopK == nil {
return nil
}
// topk max value is 6
if *props.TopK > 6 {
return utils.ToPtr(6)
}
return props.TopK
}
func (c *ChatInstance) GetMessages(props *adaptercommon.ChatProps) []Message {
var messages []Message
for _, message := range props.Message {
if message.Role == globals.Tool {
continue
}
if message.Role == globals.System {
message.Role = globals.Assistant
}
messages = append(messages, Message{
Role: message.Role,
Content: message.Content,
})
}
return messages
}
func (c *ChatInstance) GetFunctionCalling(props *adaptercommon.ChatProps) *FunctionsPayload {
if props.Tools == nil {
return nil
}
if !utils.Contains(props.Model, FunctionCallingModels) {
return nil
}
return &FunctionsPayload{
Text: utils.Each[globals.ToolObject, globals.ToolFunction](*props.Tools,
func(tool globals.ToolObject) globals.ToolFunction {
return tool.Function
}),
}
}
func getFunctionCall(call *FunctionCall) *globals.ToolCalls {
if call == nil {
return nil
}
return &globals.ToolCalls{
globals.ToolCall{
Type: "function",
Id: fmt.Sprintf("%s-%s", call.Name, call.Arguments),
Function: globals.ToolCallFunction{
Name: call.Name,
Arguments: call.Arguments,
},
},
}
}
func getChoice(form *ChatResponse) *globals.Chunk {
if form == nil || len(form.Payload.Choices.Text) == 0 {
return &globals.Chunk{Content: ""}
}
choices := form.Payload.Choices.Text
if len(choices) == 0 {
return &globals.Chunk{Content: ""}
}
choice := choices[0]
return &globals.Chunk{
Content: choice.Content,
ToolCall: getFunctionCall(choice.FunctionCall),
}
}
func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, hook globals.Hook) error {
var endpoint string
switch props.Model {
case globals.SparkDeskPro128K, globals.SparkDeskMax32K:
endpoint = fmt.Sprintf("%s/chat/%s", c.Endpoint, TransformModel(props.Model))
default:
endpoint = fmt.Sprintf("%s/%s/chat", c.Endpoint, TransformAddr(props.Model))
}
var conn *utils.WebSocket
if conn = utils.NewWebsocketClient(c.GenerateUrl(endpoint)); conn == nil {
return fmt.Errorf("sparkdesk error: websocket connection failed")
}
defer conn.DeferClose()
if err := conn.SendJSON(&ChatRequest{
Header: RequestHeader{
AppId: c.AppId,
},
Payload: RequestPayload{
Message: MessagePayload{
Text: c.GetMessages(props),
},
Functions: c.GetFunctionCalling(props),
},
Parameter: RequestParameter{
Chat: ChatParameter{
Domain: TransformModel(props.Model),
MaxToken: GetToken(props),
Temperature: props.Temperature,
TopK: GetTopK(props),
},
},
}); err != nil {
return err
}
for {
form, err := utils.ReadForm[ChatResponse](conn)
if err != nil {
if strings.Contains(err.Error(), "websocket: close 1000") {
return nil
}
globals.Debug(fmt.Sprintf("sparkdesk error: %s", err.Error()))
return nil
}
if form.Header.Code != 0 {
return fmt.Errorf("sparkdesk error: %s (sid: %s)", form.Header.Message, form.Header.Sid)
}
if err := hook(getChoice(form)); err != nil {
return err
}
}
}
================================================
FILE: adapter/sparkdesk/struct.go
================================================
package sparkdesk
import (
factory "chat/adapter/common"
"chat/globals"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"net/url"
"strings"
"time"
)
type ChatInstance struct {
AppId string
ApiSecret string
ApiKey string
Endpoint string
}
func TransformAddr(model string) string {
switch model {
case globals.SparkDeskLite:
return "v1.1"
case globals.SparkDeskPro:
return "v3.1"
case globals.SparkDeskMax:
return "v3.5"
case globals.SparkDeskV4Ultra:
return "v4.0"
default:
return "v1.1"
}
}
func TransformModel(model string) string {
switch model {
case globals.SparkDeskLite:
return "general"
case globals.SparkDeskPro:
return "generalv3"
case globals.SparkDeskPro128K:
return "pro-128k"
case globals.SparkDeskMax:
return "generalv3.5"
case globals.SparkDeskMax32K:
return "max-32k"
case globals.SparkDeskV4Ultra:
return "4.0Ultra"
default:
return "general"
}
}
func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {
params := conf.SplitRandomSecret(3)
return &ChatInstance{
AppId: params[0],
ApiSecret: params[1],
ApiKey: params[2],
Endpoint: conf.GetEndpoint(),
}
}
func (c *ChatInstance) CreateUrl(endpoint, host, date, auth string) string {
v := make(url.Values)
v.Add("host", host)
v.Add("date", date)
v.Add("authorization", auth)
return fmt.Sprintf("%s?%s", endpoint, v.Encode())
}
func (c *ChatInstance) Sign(data, key string) string {
mac := hmac.New(sha256.New, []byte(key))
mac.Write([]byte(data))
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}
// GenerateUrl will generate the signed url for sparkdesk api
func (c *ChatInstance) GenerateUrl(endpoint string) string {
uri, err := url.Parse(endpoint)
if err != nil {
return ""
}
date := time.Now().UTC().Format(time.RFC1123)
data := strings.Join([]string{
fmt.Sprintf("host: %s", uri.Host),
fmt.Sprintf("date: %s", date),
fmt.Sprintf("GET %s HTTP/1.1", uri.Path),
}, "\n")
signature := c.Sign(data, c.ApiSecret)
authorization := base64.StdEncoding.EncodeToString([]byte(
fmt.Sprintf(
"hmac username=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"",
c.ApiKey,
"hmac-sha256",
"host date request-line",
signature,
),
))
return c.CreateUrl(endpoint, uri.Host, date, authorization)
}
================================================
FILE: adapter/sparkdesk/types.go
================================================
package sparkdesk
import "chat/globals"
// ChatRequest is the request body for sparkdesk
type ChatRequest struct {
Header RequestHeader `json:"header"`
Payload RequestPayload `json:"payload"`
Parameter RequestParameter `json:"parameter"`
}
type RequestHeader struct {
AppId string `json:"app_id"`
}
type RequestPayload struct {
Message MessagePayload `json:"message"`
Functions *FunctionsPayload `json:"functions,omitempty"`
}
type FunctionsPayload struct {
Text []globals.ToolFunction `json:"text"`
}
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
FunctionCall *FunctionCall `json:"function_call,omitempty"`
}
type FunctionCall struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
}
type MessagePayload struct {
Text []Message `json:"text"`
}
type RequestParameter struct {
Chat ChatParameter `json:"chat"`
}
type ChatParameter struct {
Domain string `json:"domain"`
MaxToken *int `json:"max_tokens,omitempty"`
Temperature *float32 `json:"temperature,omitempty"`
TopK *int `json:"top_k,omitempty"`
}
// ChatResponse is the websocket partial response body for sparkdesk
type ChatResponse struct {
Header struct {
Code int `json:"code" required:"true"`
Message string `json:"message"`
Sid string `json:"sid"`
Status int `json:"status"`
} `json:"header"`
Payload struct {
Choices struct {
Status int `json:"status"`
Seq int `json:"seq"`
Text []Message `json:"text"`
} `json:"choices"`
Usage struct {
Text struct {
QuestionTokens int `json:"question_tokens"`
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}
}
}
}
================================================
FILE: adapter/zhinao/chat.go
================================================
package zhinao
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"fmt"
"strings"
)
func (c *ChatInstance) GetChatEndpoint() string {
return fmt.Sprintf("%s/v1/chat/completions", c.GetEndpoint())
}
func (c *ChatInstance) GetModel(model string) string {
switch model {
case globals.GPT360V9:
return "360GPT_S2_V9"
default:
return model
}
}
func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} {
// 2048 is the max token for 360GPT
if props.MaxTokens != nil && *props.MaxTokens > 2048 {
props.MaxTokens = utils.ToPtr(2048)
}
return ChatRequest{
Model: c.GetModel(props.Model),
Messages: utils.EachNotNil(props.Message, func(message globals.Message) *globals.Message {
if message.Role == globals.Tool {
return nil
}
return &message
}),
MaxToken: props.MaxTokens,
Stream: stream,
Temperature: props.Temperature,
TopP: props.TopP,
TopK: props.TopK,
RepetitionPenalty: props.RepetitionPenalty,
}
}
// CreateChatRequest is the native http request body for zhinao
func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {
res, err := utils.Post(
c.GetChatEndpoint(),
c.GetHeader(),
c.GetChatBody(props, false),
props.Proxy,
)
if err != nil || res == nil {
return "", fmt.Errorf("zhinao error: %s", err.Error())
}
data := utils.MapToStruct[ChatResponse](res)
if data == nil {
return "", fmt.Errorf("zhinao error: cannot parse response")
} else if data.Error.Message != "" {
return "", fmt.Errorf("zhinao error: %s", data.Error.Message)
}
return data.Choices[0].Message.Content, nil
}
// CreateStreamChatRequest is the stream response body for zhinao
func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {
buf := ""
cursor := 0
chunk := ""
err := utils.EventSource(
"POST",
c.GetChatEndpoint(),
c.GetHeader(),
c.GetChatBody(props, true),
func(data string) error {
data, err := c.ProcessLine(buf, data)
chunk += data
if err != nil {
if strings.HasPrefix(err.Error(), "zhinao error") {
return err
}
// error when break line
buf = buf + data
return nil
}
buf = ""
if data != "" {
cursor += 1
if err := callback(&globals.Chunk{Content: data}); err != nil {
return err
}
}
return nil
},
props.Proxy,
)
if err != nil {
return err
} else if len(chunk) == 0 {
return fmt.Errorf("empty response")
}
return nil
}
================================================
FILE: adapter/zhinao/processor.go
================================================
package zhinao
import (
"chat/globals"
"chat/utils"
"errors"
"fmt"
"strings"
)
func processFormat(data string) string {
rep := strings.NewReplacer(
"data: {",
"\"data\": {",
)
item := rep.Replace(data)
if !strings.HasPrefix(item, "{") {
item = "{" + item
}
if !strings.HasSuffix(item, "}}") {
item = item + "}"
}
return item
}
func processChatResponse(data string) *ChatStreamResponse {
if strings.HasPrefix(data, "{") {
var form *ChatStreamResponse
if form = utils.UnmarshalForm[ChatStreamResponse](data); form != nil {
return form
}
if form = utils.UnmarshalForm[ChatStreamResponse](data[:len(data)-1]); form != nil {
return form
}
if form = utils.UnmarshalForm[ChatStreamResponse](data + "}"); form != nil {
return form
}
}
return nil
}
func processChatErrorResponse(data string) *ChatStreamErrorResponse {
if strings.HasPrefix(data, "{") {
var form *ChatStreamErrorResponse
if form = utils.UnmarshalForm[ChatStreamErrorResponse](data); form != nil {
return form
}
if form = utils.UnmarshalForm[ChatStreamErrorResponse](data + "}"); form != nil {
return form
}
}
return nil
}
func isDone(data string) bool {
return utils.Contains[string](data, []string{
"{data: [DONE]}", "{data: [DONE]}}", "null}}", "{null}",
"{[DONE]}", "{data:}", "{data:}}", "data: [DONE]}}",
})
}
func getChoices(form *ChatStreamResponse) string {
if len(form.Data.Choices) == 0 {
return ""
}
return form.Data.Choices[0].Delta.Content
}
func (c *ChatInstance) ProcessLine(buf, data string) (string, error) {
item := processFormat(buf + data)
if isDone(item) {
return "", nil
}
if form := processChatResponse(item); form == nil {
// recursive call
if len(buf) > 0 {
return c.ProcessLine("", buf+item)
}
if err := processChatErrorResponse(item); err == nil || err.Data.Error.Message == "" {
globals.Warn(fmt.Sprintf("zhinao error: cannot parse response: %s", item))
return data, errors.New("parser error: cannot parse response")
} else {
return "", fmt.Errorf("zhinao error: %s (type: %s)", err.Data.Error.Message, err.Data.Error.Type)
}
} else {
return getChoices(form), nil
}
}
================================================
FILE: adapter/zhinao/struct.go
================================================
package zhinao
import (
factory "chat/adapter/common"
"chat/globals"
"fmt"
)
type ChatInstance struct {
Endpoint string
ApiKey string
}
func (c *ChatInstance) GetEndpoint() string {
return c.Endpoint
}
func (c *ChatInstance) GetApiKey() string {
return c.ApiKey
}
func (c *ChatInstance) GetHeader() map[string]string {
return map[string]string{
"Content-Type": "application/json",
"Authorization": fmt.Sprintf("Bearer %s", c.GetApiKey()),
}
}
func NewChatInstance(endpoint, apiKey string) *ChatInstance {
return &ChatInstance{
Endpoint: endpoint,
ApiKey: apiKey,
}
}
func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {
return NewChatInstance(
conf.GetEndpoint(),
conf.GetRandomSecret(),
)
}
================================================
FILE: adapter/zhinao/types.go
================================================
package zhinao
import "chat/globals"
// 360 ZhiNao API is similar to OpenAI API
// ChatRequest is the request body for zhinao
type ChatRequest struct {
Model string `json:"model"`
Messages []globals.Message `json:"messages"`
MaxToken *int `json:"max_tokens,omitempty"`
TopP *float32 `json:"top_p,omitempty"`
TopK *int `json:"top_k,omitempty"`
Temperature *float32 `json:"temperature,omitempty"`
RepetitionPenalty *float32 `json:"repetition_penalty,omitempty"`
Stream bool `json:"stream"`
}
// ChatResponse is the native http request body for zhinao
type ChatResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []struct {
Message struct {
Content string `json:"content"`
}
} `json:"choices"`
Error struct {
Message string `json:"message"`
} `json:"error"`
}
// ChatStreamResponse is the stream response body for zhinao
type ChatStreamResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Data struct {
Choices []struct {
Delta struct {
Content string `json:"content"`
}
Index int `json:"index"`
} `json:"choices"`
} `json:"data"`
}
type ChatStreamErrorResponse struct {
Data struct {
Error struct {
Message string `json:"message"`
Type string `json:"type"`
} `json:"error"`
} `json:"data"`
}
================================================
FILE: adapter/zhipuai/chat.go
================================================
package zhipuai
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"errors"
"fmt"
"regexp"
)
func (c *ChatInstance) GetChatEndpoint() string {
return fmt.Sprintf("%s/api/paas/v4/chat/completions", c.GetEndpoint())
}
func (c *ChatInstance) GetCompletionPrompt(messages []globals.Message) string {
result := ""
for _, message := range messages {
result += fmt.Sprintf("%s: %s\n", message.Role, message.Content)
}
return result
}
func (c *ChatInstance) GetLatestPrompt(props *adaptercommon.ChatProps) string {
if len(props.Message) == 0 {
return ""
}
return props.Message[len(props.Message)-1].Content
}
func (c *ChatInstance) ConvertModel(model string) string {
// for v3 legacy adapter
switch model {
case globals.ZhiPuChatGLMTurbo:
return GLMTurbo
case globals.ZhiPuChatGLMPro:
return GLMPro
case globals.ZhiPuChatGLMStd:
return GLMStd
case globals.ZhiPuChatGLMLite:
return GLMLite
default:
return GLMStd
}
}
func (c *ChatInstance) GetChatBody(props *adaptercommon.ChatProps, stream bool) interface{} {
if props.Model == globals.GPT3TurboInstruct {
// for completions
return CompletionRequest{
Model: c.ConvertModel(props.Model),
Prompt: c.GetCompletionPrompt(props.Message),
MaxToken: props.MaxTokens,
Stream: stream,
}
}
messages := formatMessages(props)
// chatglm top_p should be (0.0, 1.0) and cannot be 0 or 1
if props.TopP != nil && *props.TopP >= 1.0 {
props.TopP = utils.ToPtr[float32](0.99)
} else if props.TopP != nil && *props.TopP <= 0.0 {
props.TopP = utils.ToPtr[float32](0.01)
}
return ChatRequest{
Model: props.Model,
Messages: messages,
MaxToken: props.MaxTokens,
Stream: stream,
PresencePenalty: props.PresencePenalty,
FrequencyPenalty: props.FrequencyPenalty,
Temperature: props.Temperature,
TopP: props.TopP,
Tools: props.Tools,
ToolChoice: props.ToolChoice,
}
}
// CreateChatRequest is the native http request body for chatglm
func (c *ChatInstance) CreateChatRequest(props *adaptercommon.ChatProps) (string, error) {
res, err := utils.Post(
c.GetChatEndpoint(),
c.GetHeader(),
c.GetChatBody(props, false),
props.Proxy,
)
if err != nil || res == nil {
return "", fmt.Errorf("chatglm error: %s", err.Error())
}
data := utils.MapToStruct[ChatResponse](res)
if data == nil {
return "", fmt.Errorf("chatglm error: cannot parse response")
} else if data.Error.Message != "" {
return "", fmt.Errorf("chatglm error: %s", data.Error.Message)
}
return data.Choices[0].Message.Content, nil
}
func hideRequestId(message string) string {
// xxx (request id: 2024020311120561344953f0xfh0TX)
exp := regexp.MustCompile(`\(request id: [a-zA-Z0-9]+\)`)
return exp.ReplaceAllString(message, "")
}
// CreateStreamChatRequest is the stream response body for chatglm
func (c *ChatInstance) CreateStreamChatRequest(props *adaptercommon.ChatProps, callback globals.Hook) error {
ticks := 0
err := utils.EventScanner(&utils.EventScannerProps{
Method: "POST",
Uri: c.GetChatEndpoint(),
Headers: c.GetHeader(),
Body: c.GetChatBody(props, true),
Callback: func(data string) error {
ticks += 1
partial, err := c.ProcessLine(data, false)
if err != nil {
return err
}
return callback(partial)
},
}, props.Proxy)
if err != nil {
if form := processChatErrorResponse(err.Body); form != nil {
if form.Error.Type == "" && form.Error.Message == "" {
return errors.New(utils.ToMarkdownCode("json", err.Body))
}
msg := fmt.Sprintf("%s (code: %s)", form.Error.Message, form.Error.Code)
return errors.New(hideRequestId(msg))
}
return err.Error
}
if ticks == 0 {
return errors.New("no response")
}
return nil
}
================================================
FILE: adapter/zhipuai/processor.go
================================================
package zhipuai
import (
adaptercommon "chat/adapter/common"
"chat/globals"
"chat/utils"
"errors"
"fmt"
"regexp"
"strings"
)
func formatMessages(props *adaptercommon.ChatProps) interface{} {
if globals.IsVisionModel(props.Model) {
return utils.Each[globals.Message, Message](props.Message, func(message globals.Message) Message {
if message.Role == globals.User {
content, urls := utils.ExtractImages(message.Content, true)
images := utils.EachNotNil[string, MessageContent](urls, func(url string) *MessageContent {
obj, err := utils.NewImage(url)
props.Buffer.AddImage(obj)
if err != nil {
globals.Info(fmt.Sprintf("cannot process image: %s (source: %s)", err.Error(), utils.Extract(url, 24, "...")))
}
if strings.HasPrefix(url, "data:image/") {
// remove base64 image prefix
if idx := strings.Index(url, "base64,"); idx != -1 {
url = url[idx+7:]
}
}
return &MessageContent{
Type: "image_url",
ImageUrl: &ImageUrl{
Url: url,
},
}
})
return Message{
Role: message.Role,
Content: utils.Prepend(images, MessageContent{
Type: "text",
Text: &content,
}),
Name: message.Name,
FunctionCall: message.FunctionCall,
ToolCalls: message.ToolCalls,
ToolCallId: message.ToolCallId,
}
}
return Message{
Role: message.Role,
Content: message.Content,
Name: message.Name,
FunctionCall: message.FunctionCall,
ToolCalls: message.ToolCalls,
ToolCallId: message.ToolCallId,
}
})
}
return props.Message
}
func processChatResponse(data string) *ChatStreamResponse {
return utils.UnmarshalForm[ChatStreamResponse](data)
}
func processCompletionResponse(data string) *CompletionResponse {
return utils.UnmarshalForm[CompletionResponse](data)
}
func processChatErrorResponse(data string) *ChatStreamErrorResponse {
return utils.UnmarshalForm[ChatStreamErrorResponse](data)
}
func getChoices(form *ChatStreamResponse) *globals.Chunk {
if len(form.Choices) == 0 {
return &globals.Chunk{Content: ""}
}
choice := form.Choices[0].Delta
return &globals.Chunk{
Content: choice.Content,
ToolCall: choice.ToolCalls,
FunctionCall: choice.FunctionCall,
}
}
func getCompletionChoices(form *CompletionResponse) string {
if len(form.Choices) == 0 {
return ""
}
return form.Choices[0].Text
}
func getRobustnessResult(chunk string) string {
exp := `\"content\":\"(.*?)\"`
compile, err := regexp.Compile(exp)
if err != nil {
return ""
}
matches := compile.FindStringSubmatch(chunk)
if len(matches) > 1 {
return utils.ProcessRobustnessChar(matches[1])
} else {
return ""
}
}
func (c *ChatInstance) ProcessLine(data string, isCompletionType bool) (*globals.Chunk, error) {
if isCompletionType {
// chatglm legacy support
if completion := processCompletionResponse(data); completion != nil {
return &globals.Chunk{
Content: getCompletionChoices(completion),
}, nil
}
globals.Warn(fmt.Sprintf("chatglm error: cannot parse completion response: %s", data))
return &globals.Chunk{Content: ""}, errors.New("parser error: cannot parse completion response")
}
if form := processChatResponse(data); form != nil {
return getChoices(form), nil
}
if form := processChatErrorResponse(data); form != nil {
return &globals.Chunk{Content: ""}, errors.New(fmt.Sprintf("chatglm error: %s (type: %s)", form.Error.Message, form.Error.Type))
}
globals.Warn(fmt.Sprintf("chatglm error: cannot parse chat completion response: %s", data))
return &globals.Chunk{Content: ""}, errors.New("parser error: cannot parse chat completion response")
}
================================================
FILE: adapter/zhipuai/struct.go
================================================
package zhipuai
import (
factory "chat/adapter/common"
"chat/globals"
"chat/utils"
"fmt"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
)
type ChatInstance struct {
Endpoint string
ApiKey string
}
type Payload struct {
ApiKey string `json:"api_key"`
Exp int64 `json:"exp"`
TimeStamp int64 `json:"timestamp"`
}
func (c *ChatInstance) GetEndpoint() string {
return c.Endpoint
}
func (c *ChatInstance) GetApiKey() string {
return c.ApiKey
}
func (c *ChatInstance) GetHeader() map[string]string {
return map[string]string{
"Content-Type": "application/json",
"Authorization": fmt.Sprintf("Bearer %s", c.GetToken()),
}
}
func (c *ChatInstance) GetToken() string {
// get jwt token for zhipuai api
segment := strings.Split(c.ApiKey, ".")
if len(segment) != 2 {
return ""
}
id, secret := segment[0], segment[1]
payload := utils.MapToStruct[jwt.MapClaims](Payload{
ApiKey: id,
Exp: time.Now().Add(time.Minute*5).Unix() * 1000,
TimeStamp: time.Now().Unix() * 1000,
})
instance := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
instance.Header = map[string]interface{}{
"alg": "HS256",
"sign_type": "SIGN",
}
token, _ := instance.SignedString([]byte(secret))
return token
}
func NewChatInstance(endpoint, apiKey string) *ChatInstance {
return &ChatInstance{
Endpoint: endpoint,
ApiKey: apiKey,
}
}
func NewChatInstanceFromConfig(conf globals.ChannelConfig) factory.Factory {
return NewChatInstance(
conf.GetEndpoint(),
conf.GetRandomSecret(),
)
}
================================================
FILE: adapter/zhipuai/types.go
================================================
package zhipuai
import "chat/globals"
const (
GLM4 = "glm-4"
GLM4Vision = "glm-4v"
GLMTurbo = "glm-3-turbo" // GLM3 Turbo
GLMPro = "chatglm_pro" // GLM3 Pro (deprecated)
GLMStd = "chatglm_std" // GLM3 Standard (deprecated)
GLMLite = "chatglm_lite" // GLM3 Lite (deprecated)
)
type ImageUrl struct {
Url string `json:"url"`
Detail *string `json:"detail,omitempty"`
}
type MessageContent struct {
Type string `json:"type"`
Text *string `json:"text,omitempty"`
ImageUrl *ImageUrl `json:"image_url,omitempty"`
}
type MessageContents []MessageContent
type Message struct {
Role string `json:"role"`
Content interface{} `json:"content"`
Name *string `json:"name,omitempty"`
FunctionCall *globals.FunctionCall `json:"function_call,omitempty"` // only `function` role
ToolCallId *string `json:"tool_call_id,omitempty"` // only `tool` role
ToolCalls *globals.ToolCalls `json:"tool_calls,omitempty"` // only `assistant` role
}
// ChatRequest is the request body for chatglm
type ChatRequest struct {
Model string `json:"model"`
Messages interface{} `json:"messages"`
MaxToken *int `json:"max_tokens,omitempty"`
Stream bool `json:"stream"`
PresencePenalty *float32 `json:"presence_penalty,omitempty"`
FrequencyPenalty *float32 `json:"frequency_penalty,omitempty"`
Temperature *float32 `json:"temperature,omitempty"`
TopP *float32 `json:"top_p,omitempty"`
Tools *globals.FunctionTools `json:"tools,omitempty"`
ToolChoice *interface{} `json:"tool_choice,omitempty"` // string or object
}
// CompletionRequest is the request body for chatglm completion
type CompletionRequest struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
MaxToken *int `json:"max_tokens,omitempty"`
Stream bool `json:"stream"`
}
// ChatResponse is the native http request body for chatglm
type ChatResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []struct {
Index int `json:"index"`
Message globals.Message `json:"message"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
Error struct {
Message string `json:"message"`
} `json:"error"`
}
// ChatStreamResponse is the stream response body for chatglm
type ChatStreamResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []struct {
Delta globals.Message `json:"delta"`
Index int `json:"index"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
}
// CompletionResponse is the native http request body / stream response body for chatglm completion
type CompletionResponse struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
Model string `json:"model"`
Choices []struct {
Text string `json:"text"`
Index int `json:"index"`
} `json:"choices"`
}
type ChatStreamErrorResponse struct {
Error struct {
Message string `json:"message"`
Type string `json:"type"`
Code string `json:"code"`
} `json:"error"`
}
type ImageSize string
// ImageRequest is the request body for chatglm dalle image generation
type ImageRequest struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
Size ImageSize `json:"size"`
N int `json:"n"`
}
type ImageResponse struct {
Data []struct {
Url string `json:"url"`
} `json:"data"`
Error struct {
Message string `json:"message"`
} `json:"error"`
}
var (
ImageSize256 ImageSize = "256x256"
ImageSize512 ImageSize = "512x512"
ImageSize1024 ImageSize = "1024x1024"
)
================================================
FILE: addition/article/api.go
================================================
package article
import (
"chat/auth"
"chat/globals"
"chat/utils"
"fmt"
"github.com/gin-gonic/gin"
"strings"
)
type WebsocketArticleForm struct {
Token string `json:"token" binding:"required"`
Model string `json:"model" binding:"required"`
Prompt string `json:"prompt" binding:"required"`
Title string `json:"title" binding:"required"`
Web bool `json:"web"`
}
type WebsocketArticleResponse struct {
Hash string `json:"hash"`
Data StreamProgressResponse `json:"data"`
}
func ProjectTarDownloadAPI(c *gin.Context) {
hash := strings.TrimSpace(c.Query("hash"))
c.Writer.Header().Add("Content-Disposition", "attachment; filename=article.tar.gz")
c.File(fmt.Sprintf("storage/article/%s.tar.gz", hash))
}
func ProjectZipDownloadAPI(c *gin.Context) {
hash := strings.TrimSpace(c.Query("hash"))
c.Writer.Header().Add("Content-Disposition", "attachment; filename=article.zip")
c.File(fmt.Sprintf("storage/article/%s.zip", hash))
}
func GenerateAPI(c *gin.Context) {
var conn *utils.WebSocket
if conn = utils.NewWebsocket(c, false); conn == nil {
return
}
defer conn.DeferClose()
form, err := utils.ReadForm[WebsocketArticleForm](conn)
if err != nil {
return
}
user := auth.ParseToken(c, form.Token)
db := utils.GetDBFromContext(c)
if !auth.HitGroups(db, user, globals.ArticlePermissionGroup) {
return
}
if len(form.Title) == 0 {
return
}
hash := CreateWorker(c, user, form.Model, form.Prompt, form.Title, form.Web, func(resp StreamProgressResponse) {
conn.Send(WebsocketArticleResponse{
Hash: "",
Data: resp,
})
})
conn.Send(WebsocketArticleResponse{
Hash: hash,
})
}
================================================
FILE: addition/article/data/.gitkeep
================================================
================================================
FILE: addition/article/generate.go
================================================
package article
import (
"chat/auth"
"chat/globals"
"chat/manager"
"chat/utils"
"fmt"
"github.com/gin-gonic/gin"
"strings"
)
type StreamProgressResponse struct {
Current int `json:"current"`
Total int `json:"total"`
Quota float32 `json:"quota"`
}
type Response struct {
File string
Quota float32
}
func GenerateArticle(c *gin.Context, user *auth.User, model string, hash string, title string, prompt string, enableWeb bool) Response {
message, quota := manager.NativeChatHandler(c, user, model, []globals.Message{{
Role: globals.User,
Content: fmt.Sprintf("%s\n%s", prompt, title),
}}, enableWeb)
return Response{
File: CreateArticleFile(hash, title, message),
Quota: quota,
}
}
func ParseTitle(titles string) []string {
var result []string
for _, title := range strings.Split(titles, "\n") {
title = strings.TrimSpace(title)
if len(title) > 0 {
result = append(result, title)
}
}
return result
}
func CreateGenerationWorker(c *gin.Context, user *auth.User, model string, prompt string, title string, enableWeb bool, hash string) (int, chan Response) {
titles := ParseTitle(title)
result := make(chan Response, len(titles))
for _, name := range titles {
go func(title string) {
result <- GenerateArticle(c, user, model, hash, title, prompt, enableWeb)
}(name)
}
return len(titles), result
}
func CreateWorker(c *gin.Context, user *auth.User, model string, prompt string, title string, enableWeb bool, hook func(resp StreamProgressResponse)) string {
hash := utils.Md5Encrypt(fmt.Sprintf("%s%s%s%v", model, prompt, title, enableWeb))
total, channel := CreateGenerationWorker(c, user, model, prompt, title, enableWeb, hash)
current := 0
hook(StreamProgressResponse{Current: current, Total: total, Quota: 0})
for resp := range channel {
current += 1
hook(StreamProgressResponse{Current: current, Total: total, Quota: resp.Quota})
if current == total {
break
}
}
hook(StreamProgressResponse{Current: current, Total: total, Quota: 0})
path := fmt.Sprintf("storage/article/data/%s", hash)
if _, _, err := utils.GenerateCompressTask(hash, "storage/article", path, path); err != nil {
globals.Debug(fmt.Sprintf("[article] error during generate compress task: %s", err.Error()))
return ""
}
return hash
}
================================================
FILE: addition/article/utils.go
================================================
package article
import (
"chat/globals"
"chat/utils"
"fmt"
"github.com/lukasjarosch/go-docx"
)
func GenerateDocxFile(target, title, content string) error {
data := docx.PlaceholderMap{
"title": title,
"content": content,
}
doc, err := docx.Open("addition/article/template.docx")
if err != nil {
return err
}
if err := doc.ReplaceAll(data); err != nil {
return err
}
if err := doc.WriteToFile(target); err != nil {
return err
}
return nil
}
func CreateArticleFile(hash, title, content string) string {
target := fmt.Sprintf("storage/article/data/%s/%s.docx", hash, title)
utils.FileDirSafe(target)
if err := GenerateDocxFile(target, title, content); err != nil {
globals.Debug(fmt.Sprintf("[article] error during generate article %s: %s", title, err.Error()))
}
return target
}
================================================
FILE: addition/card/.gitignore
================================================
.idea
.vscode
================================================
FILE: addition/card/card.go
================================================
package card
import (
"chat/globals"
"chat/manager"
"github.com/gin-gonic/gin"
"github.com/russross/blackfriday/v2"
"net/http"
"strings"
)
type RequestForm struct {
Message string `json:"message" required:"true"`
Web bool `json:"web"`
}
const maxColumnPerLine = 50
func ProcessMarkdownLine(source []byte) string {
segment := strings.Split(string(source), "\n")
var result []rune
for _, line := range segment {
data := []rune(line)
length := len([]rune(line))
if length < maxColumnPerLine {
result = append(result, data...)
result = append(result, '\n')
} else {
for i := 0; i < length; i += maxColumnPerLine {
if i+maxColumnPerLine < length {
result = append(result, data[i:i+maxColumnPerLine]...)
result = append(result, '\n')
} else {
result = append(result, data[i:]...)
result = append(result, '\n')
}
}
}
}
return string(result)
}
func MarkdownConvert(text string) string {
if text == "" {
return ""
}
result := blackfriday.Run([]byte(text))
return string(result)
}
func HandlerAPI(c *gin.Context) {
var body RequestForm
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid request body",
})
}
message := strings.TrimSpace(body.Message)
if len(message) == 0 {
c.JSON(http.StatusBadRequest, gin.H{
"error": "message is empty",
})
return
}
response, quota := manager.NativeChatHandler(c, nil, globals.GPT3Turbo0613, []globals.Message{
{Role: globals.User, Content: message},
}, body.Web)
c.JSON(http.StatusOK, gin.H{
"message": MarkdownConvert(response),
"keyword": "",
"quota": quota,
})
}
================================================
FILE: addition/card/card.php
================================================
", " ", $resp['message']));
$msgHeight = substr_count($resp['message'], "\n") * 20 + strlen($msg) * 0.15 + substr_count($resp['message'], "
ChatGPT
ChatGPT Card
chatnio
OpenAI
================================================
FILE: addition/card/error.php
================================================
Error
Error Card
================================================
FILE: addition/card/utils.php
================================================
[^\S ]+/', '/[^\S ]+', '/(\s)+/', '/> ', '/:\s+/', '/\{\s+/', '/\s+}/');
$replace = array('>', '<', '\\1', '><', ':', '{', '}');
return preg_replace($search, $replace, $buffer);
}
function fetch($message, $web): array|string|null
{
$opts = array('http' =>
array(
'method' => 'POST',
'header' => 'Content-type: application/json',
'content' => json_encode(array('message' => $message, 'web' => $web))
)
);
$context = stream_context_create($opts);
$response = @file_get_contents("http://localhost:8094/card", false, $context);
$ok = $response !== false;
return $ok ? json_decode($response, true) : null;
}
function get($param, $default = null)
{
return $_GET[$param] ?? $default;
}
================================================
FILE: addition/generation/api.go
================================================
package generation
import (
"chat/auth"
"chat/globals"
"chat/utils"
"fmt"
"strings"
"github.com/gin-gonic/gin"
)
type WebsocketGenerationForm struct {
Token string `json:"token" binding:"required"`
Prompt string `json:"prompt" binding:"required"`
Model string `json:"model" binding:"required"`
}
func ProjectTarDownloadAPI(c *gin.Context) {
hash := strings.TrimSpace(c.Query("hash"))
c.Writer.Header().Add("Content-Disposition", "attachment; filename=code.tar.gz")
c.File(fmt.Sprintf("storage/generation/%s.tar.gz", hash))
}
func ProjectZipDownloadAPI(c *gin.Context) {
hash := strings.TrimSpace(c.Query("hash"))
c.Writer.Header().Add("Content-Disposition", "attachment; filename=code.zip")
c.File(fmt.Sprintf("storage/generation/%s.zip", hash))
}
func GenerateAPI(c *gin.Context) {
var conn *utils.WebSocket
if conn = utils.NewWebsocket(c, false); conn == nil {
return
}
defer conn.DeferClose()
form, err := utils.ReadForm[WebsocketGenerationForm](conn)
if err != nil {
return
}
user := auth.ParseToken(c, form.Token)
db := utils.GetDBFromContext(c)
cache := utils.GetCacheFromContext(c)
if !auth.HitGroups(db, user, globals.GenerationPermissionGroup) {
conn.Send(globals.GenerationSegmentResponse{
Message: "permission denied",
Quota: 0,
End: true,
})
return
}
check, plan := auth.CanEnableModelWithSubscription(db, cache, user, form.Model, []globals.Message{})
if check != nil {
conn.Send(globals.GenerationSegmentResponse{
Message: check.Error(),
Quota: 0,
End: true,
})
return
}
var instance *utils.Buffer
hash, err := CreateGenerationWithCache(
auth.GetGroup(db, user),
form.Model,
form.Prompt,
func(buffer *utils.Buffer, data string) {
instance = buffer
conn.Send(globals.GenerationSegmentResponse{
End: false,
Message: data,
Quota: buffer.GetQuota(),
})
},
)
if instance != nil && !plan && instance.GetQuota() > 0 && user != nil {
user.UseQuota(db, instance.GetQuota())
}
if err != nil {
auth.RevertSubscriptionUsage(db, cache, user, form.Model)
conn.Send(globals.GenerationSegmentResponse{
End: true,
Error: err.Error(),
Quota: instance.GetQuota(),
})
return
}
conn.Send(globals.GenerationSegmentResponse{
End: true,
Hash: hash,
Quota: instance.GetQuota(),
})
}
================================================
FILE: addition/generation/build.go
================================================
package generation
import (
"chat/utils"
"fmt"
"time"
)
func GetFolder(hash string) string {
return fmt.Sprintf("storage/generation/data/%s", hash)
}
func GetFolderByHash(model string, prompt string) (string, string) {
hash := utils.Sha2Encrypt(model + prompt + time.Now().Format("2006-01-02 15:04:05"))
return hash, GetFolder(hash)
}
func GenerateProject(path string, instance ProjectResult) bool {
for name, data := range instance.Result {
current := fmt.Sprintf("%s/%s", path, name)
if content, ok := data.(string); ok {
if utils.WriteFile(current, content, true) != nil {
return false
}
} else {
GenerateProject(current, ProjectResult{
Result: data.(map[string]interface{}),
})
}
}
return true
}
================================================
FILE: addition/generation/data/.gitkeep
================================================
================================================
FILE: addition/generation/generate.go
================================================
package generation
import (
"chat/globals"
"chat/utils"
"fmt"
)
func CreateGenerationWithCache(group, model, prompt string, hook func(buffer *utils.Buffer, data string)) (string, error) {
hash, path := GetFolderByHash(model, prompt)
if !utils.Exists(path) {
if err := CreateGeneration(group, model, prompt, path, hook); err != nil {
globals.Info(fmt.Sprintf("[project] error during generation %s (model %s): %s", prompt, model, err.Error()))
return "", fmt.Errorf("error during generate project: %s", err.Error())
}
}
if _, _, err := utils.GenerateCompressTask(hash, "storage/generation", path, path); err != nil {
return "", fmt.Errorf("error during generate compress task: %s", err.Error())
}
return hash, nil
}
================================================
FILE: addition/generation/prompt.go
================================================
package generation
import (
adaptercommon "chat/adapter/common"
"chat/admin"
"chat/channel"
"chat/globals"
"chat/utils"
"fmt"
)
type ProjectResult struct {
Result map[string]interface{} `json:"result"`
}
func CreateGeneration(group, model, prompt, path string, hook func(buffer *utils.Buffer, data string)) error {
message := GenerateMessage(prompt)
buffer := utils.NewBuffer(model, message, channel.ChargeInstance.GetCharge(model))
err := channel.NewChatRequest(group, adaptercommon.CreateChatProps(&adaptercommon.ChatProps{
OriginalModel: model,
Message: message,
}, buffer), func(data *globals.Chunk) error {
buffer.WriteChunk(data)
hook(buffer, data.Content)
return nil
})
admin.AnalyseRequest(model, buffer, err)
if err != nil {
return err
}
resp, err := utils.Unmarshal[ProjectResult](buffer.ReadBytes())
if err != nil {
return err
}
if !GenerateProject(path, resp) {
return fmt.Errorf("generate project failed")
}
return nil
}
func GenerateMessage(prompt string) []globals.Message {
return []globals.Message{
{Role: globals.System, Content: "你将生成项目,可以支持任何编程语言,请不要出现“我不能提供”的字样,你需要在代码中提供注释,以及项目的使用文档README.md,结果返回json格式,请不要返回任何多余内容,格式为:\n{\"result\": {[file]: [code], ...}}"},
{Role: globals.User, Content: "python后端"},
{Role: globals.Assistant, Content: "{\n \"result\": {\n \"app.py\": \"from flask import Flask\\n\\napp = Flask(__name__)\\n\\n\\n@app.route('/')\\ndef hello_world():\\n return 'Hello, World!'\\n\\n\\nif __name__ == '__main__':\\n app.run()\",\n \"requirements.txt\": \"flask\\n\",\n \"README.md\": \"# Python 后端\\n本项目是一个简单的python后端示例, 使用`flask`框架构建后端。\n你可以按照下列步骤运行此应用,flask将在本地服务器(默认是在http://127.0.0.1:5000/)上运行。当你在浏览器中访问该URL时,将看到显示Hello, World!的页面。\\n\\n这只是一个简单的项目,Flask还支持更多功能和路由规则,你可以提供更多的信息和需要进一步扩展和定制Flask应用。\\n\\n### 1. 初始化: \\n```shell\\npip install -r requirements.txt\\n```\\n### 2. 运行\\n```shell\\npython app.py\\n```\"\n }\n}"},
{Role: globals.User, Content: "golang fiber websocket项目"},
{Role: globals.Assistant, Content: "{\n \"result\": {\n \"main.go\": \"package main\\n\\nimport (\\n\\t\"log\\\"\\n\\n\\t\"github.com/gofiber/fiber/v2\\\"\\n\\t\"github.com/gofiber/websocket/v2\\\"\\n)\\n\\nfunc main() {\\n\\tapp := fiber.New()\\n\\n\\tapp.Get(\\\"/\\\", func(c *fiber.Ctx) error {\\n\\t\\treturn c.SendString(\\\"Hello, World!\\\")\\n\\t})\\n\\n\\tapp.Get(\\\"/ws\\\", websocket.New(func(c *websocket.Conn) {\\n\\t\\tfor {\\n\\t\\t\\tmt, message, err := c.ReadMessage()\\n\\t\\t\\tif err != nil {\\n\\t\\t\\t\\tlog.Println(\\\"read error:\\\", err)\\n\\t\\t\\t\\tbreak\\n\\t\\t\\t}\\n\\t\\t\\tlog.Printf(\\\"received: %s\\\", message)\\n\\t\\t\\terr = c.WriteMessage(mt, message)\\n\\t\\t\\tif err != nil {\\n\\t\\t\\t\\tlog.Println(\\\"write error:\\\", err)\\n\\t\\t\\t\\tbreak\\n\\t\\t\\t}\\n\\t\\t}\\n\\t}))\\n\\n\\tlog.Fatal(app.Listen(\\\":3000\\\"))\\n}\",\n \"go.mod\": \"module fiber-websocket\\n\\ngo 1.16\\n\\nrequire (\\n\\tgithub.com/gofiber/fiber/v2 v2.12.1\\n\\tgithub.com/gofiber/websocket/v2 v2.10.2\\n)\",\n \"README.md\": \"# Golang Fiber WebSocket项目\\n\\n这个项目是一个使用Golang和Fiber框架构建的WebSocket服务器示例。\\n\\n### 1. 初始化:\\n```shell\\ngo mod init fiber-websocket\\n```\\n\\n### 2. 安装依赖:\\n```shell\\ngo get github.com/gofiber/fiber/v2\\n``` \\n```shell\\ngo get github.com/gofiber/websocket/v2\\n```\\n\\n### 3. 创建main.go文件,将以下代码复制粘贴:\\n\\n```go\\npackage main\\n\\nimport (\\n\\t\\\"log\\\"\\n\\n\\t\\\"github.com/gofiber/fiber/v2\\\"\\n\\t\\\"github.com/gofiber/websocket/v2\\\"\\n)\\n\\nfunc main() {\\n\\tapp := fiber.New()\\n\\n\\tapp.Get(\\\"/\\\", func(c *fiber.Ctx) error {\\n\\t\\treturn c.SendString(\\\"Hello, World!\\\")\\n\\t})\\n\\n\\tapp.Get(\\\"/ws\\\", websocket.New(func(c *websocket.Conn) {\\n\\t\\tfor {\\n\\t\\t\\tmt, message, err := c.ReadMessage()\\n\\t\\t\\tif err != nil {\\n\\t\\t\\t\\tlog.Println(\\\"read error:\\\", err)\\n\\t\\t\\t\\tbreak\\n\\t\\t\\t}\\n\\t\\t\\tlog.Printf(\\\"received: %s\\\", message)\\n\\t\\t\\terr = c.WriteMessage(mt, message)\\n\\t\\t\\tif err != nil {\\n\\t\\t\\t\\tlog.Println(\\\"write error:\\\", err)\\n\\t\\t\\t\\tbreak\\n\\t\\t\\t}\\n\\t\\t}\\n\\t}))\\n\\n\\tlog.Fatal(app.Listen(\\\":3000\\\"))\\n}\\n```\\n\\n### 4. 运行应用程序:\\n```shell\\ngo run main.go\\n```\\n\\n应用程序将在本地服务器(默认是在http://localhost:3000)上运行。当你在浏览器中访问`http://localhost:3000`时,将看到显示\"Hello, World!\"的页面。你还可以访问`http://localhost:3000/ws`来测试WebSocket功能。\n\n这只是一个简单的示例,Fiber框架提供了更多的功能和路由规则,你可以在此基础上进行进一步扩展和定制。\n\n注意:在运行应用程序之前,请确保已经安装了Go语言开发环境。"},
{Role: globals.User, Content: prompt},
}
}
================================================
FILE: addition/router.go
================================================
package addition
import (
"chat/addition/article"
"chat/addition/card"
"chat/addition/generation"
"github.com/gin-gonic/gin"
)
func Register(app *gin.RouterGroup) {
{
app.POST("/card", card.HandlerAPI)
app.GET("/generation/create", generation.GenerateAPI)
app.GET("/generation/download/tar", generation.ProjectTarDownloadAPI)
app.GET("/generation/download/zip", generation.ProjectZipDownloadAPI)
app.GET("/article/create", article.GenerateAPI)
app.GET("/article/download/tar", article.ProjectTarDownloadAPI)
app.GET("/article/download/zip", article.ProjectZipDownloadAPI)
}
}
================================================
FILE: addition/web/call.go
================================================
package web
import (
"chat/globals"
"chat/manager/conversation"
"chat/utils"
"fmt"
"time"
)
type Hook func(message []globals.Message, token int) (string, error)
func toWebSearchingMessage(message []globals.Message) []globals.Message {
data, _ := GenerateSearchResult(message[len(message)-1].Content)
return utils.Insert(message, 0, globals.Message{
Role: globals.System,
Content: fmt.Sprintf("You will play the role of an AI Q&A assistant, where your knowledge base is not offline, but can be networked in real time, and you can provide real-time networked information with links to networked search sources."+
"Current time: %s, Real-time internet search results: %s",
time.Now().Format("2006-01-02 15:04:05"), data,
),
})
}
func ToChatSearched(instance *conversation.Conversation, restart bool) []globals.Message {
segment := conversation.CopyMessage(instance.GetChatMessage(restart))
if instance.IsEnableWeb() {
segment = toWebSearchingMessage(segment)
}
return segment
}
func ToSearched(enable bool, message []globals.Message) []globals.Message {
if enable {
return toWebSearchingMessage(message)
}
return message
}
================================================
FILE: addition/web/search.go
================================================
package web
import (
"chat/globals"
"chat/utils"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/gin-gonic/gin"
)
type SearXNGResponse struct {
Query string `json:"query"`
NumberOfResults int `json:"number_of_results"`
Results []struct {
Url string `json:"url"`
Title string `json:"title"`
Content string `json:"content"`
PublishedDate *string `json:"publishedDate,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
Engine string `json:"engine"`
ParsedUrl []string `json:"parsed_url"`
Template string `json:"template"`
Engines []string `json:"engines"`
Positions []int `json:"positions"`
Score float64 `json:"score"`
Category string `json:"category"`
IframeSrc string `json:"iframe_src,omitempty"`
} `json:"results"`
Answers []interface{} `json:"answers"`
Corrections []interface{} `json:"corrections"`
Infoboxes []interface{} `json:"infoboxes"`
Suggestions []interface{} `json:"suggestions"`
UnresponsiveEngines [][]string `json:"unresponsive_engines"`
}
func formatResponse(data *SearXNGResponse) string {
res := make([]string, 0)
for _, item := range data.Results {
if item.Content == "" || item.Url == "" || item.Title == "" {
continue
}
res = append(res, fmt.Sprintf("%s (%s): %s", item.Title, item.Url, item.Content))
}
return strings.Join(res, "\n")
}
func createURLParams(query string) string {
params := url.Values{}
params.Add("q", query)
params.Add("format", "json")
params.Add("safesearch", strconv.Itoa(globals.SearchSafeSearch))
if len(globals.SearchEngines) > 0 {
params.Add("engines", globals.SearchEngines)
}
if len(globals.SearchImageProxy) > 0 {
params.Add("image_proxy", globals.SearchImageProxy)
}
return fmt.Sprintf("%s?%s", globals.SearchEndpoint, params.Encode())
}
func createSearXNGRequest(query string) (*SearXNGResponse, error) {
data, err := utils.Get(createURLParams(query), nil)
if err != nil {
return nil, err
}
return utils.MapToRawStruct[SearXNGResponse](data)
}
func GenerateSearchResult(q string) (string, error) {
res, err := createSearXNGRequest(q)
if err != nil {
globals.Warn(fmt.Sprintf("[web] failed to get search result: %s (query: %s)", err.Error(), utils.Extract(q, 20, "...")))
content := fmt.Sprintf("search failed: %s", err.Error())
return content, errors.New(content)
}
content := formatResponse(res)
globals.Debug(fmt.Sprintf("[web] search result: %s (query: %s)", utils.Extract(content, 50, "..."), q))
if globals.SearchCrop {
globals.Debug(fmt.Sprintf("[web] crop search result length %d to %d max", len(content), globals.SearchCropLength))
return utils.Extract(content, globals.SearchCropLength, "..."), nil
}
return content, nil
}
func TestSearch(c *gin.Context) {
// get `query` param from query
query := c.Query("query")
fmt.Println(query)
res, err := GenerateSearchResult(query)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"error": err.Error(),
})
} else {
c.JSON(http.StatusOK, gin.H{
"status": true,
"result": res,
})
}
}
================================================
FILE: admin/analysis/analysis.go
================================================
package analysis
import (
"chat/channel"
"chat/globals"
"chat/utils"
"database/sql"
"time"
"github.com/go-redis/redis/v8"
)
type UserTypeForm struct {
Normal int64 `json:"normal"`
ApiPaid int64 `json:"api_paid"`
BasicPlan int64 `json:"basic_plan"`
StandardPlan int64 `json:"standard_plan"`
ProPlan int64 `json:"pro_plan"`
Total int64 `json:"total"`
}
func getDates(t []time.Time) []string {
return utils.Each[time.Time, string](t, func(date time.Time) string {
return date.Format("1/2")
})
}
func getFormat(t time.Time) string {
return t.Format("2006-01-02")
}
func getMinuteFormat(t time.Time) string {
return t.Format("2006-01-02 15:04")
}
func GetSubscriptionUsers(db *sql.DB) int64 {
var count int64
err := globals.QueryRowDb(db, `
SELECT COUNT(*) FROM subscription WHERE expired_at > NOW()
`).Scan(&count)
if err != nil {
return 0
}
return count
}
func GetBillingToday(cache *redis.Client) float32 {
return float32(utils.MustInt(cache, getBillingFormat(getDay()))) / 100
}
func GetBillingYesterday(cache *redis.Client) float32 {
return float32(utils.MustInt(cache, getBillingFormat(getLastDay()))) / 100
}
func GetBillingMonth(cache *redis.Client) float32 {
return float32(utils.MustInt(cache, getMonthBillingFormat(getMonth()))) / 100
}
func GetBillingLastMonth(cache *redis.Client) float32 {
return float32(utils.MustInt(cache, getMonthBillingFormat(getLastMonth()))) / 100
}
func GetTpmToday(cache *redis.Client, user string) int64 {
// this minute and last minute
return utils.MustInt(cache, getTpmFormat(getMinuteFormat(time.Now()), user)) +
utils.MustInt(cache, getTpmFormat(getMinuteFormat(time.Now().Add(-time.Minute)), user))
}
func GetRpmToday(cache *redis.Client, user string) int64 {
// this minute and last minute
return utils.MustInt(cache, getRpmFormat(getMinuteFormat(time.Now()), user)) +
utils.MustInt(cache, getRpmFormat(getMinuteFormat(time.Now().Add(-time.Minute)), user))
}
func GetModelData(cache *redis.Client) ModelChartForm {
dates := getDays(7)
return ModelChartForm{
Date: getDates(dates),
Value: utils.EachNotNil[string, ModelData](globals.SupportModels, func(model string) *ModelData {
data := ModelData{
Model: model,
Data: utils.Each[time.Time, int64](dates, func(date time.Time) int64 {
return utils.MustInt(cache, getModelFormat(getFormat(date), model))
}),
}
if utils.Sum(data.Data) == 0 {
return nil
}
return &data
}),
}
}
func GetSortedModelData(cache *redis.Client) ModelChartForm {
form := GetModelData(cache)
data := utils.Sort(form.Value, func(a ModelData, b ModelData) bool {
return utils.Sum(a.Data) > utils.Sum(b.Data)
})
form.Value = data
return form
}
func GetRequestData(cache *redis.Client) RequestChartForm {
dates := getDays(7)
return RequestChartForm{
Date: getDates(dates),
Value: utils.Each[time.Time, int64](dates, func(date time.Time) int64 {
return utils.MustInt(cache, getRequestFormat(getFormat(date)))
}),
}
}
func GetBillingData(cache *redis.Client) BillingChartForm {
dates := getDays(30)
return BillingChartForm{
Date: getDates(dates),
Value: utils.Each[time.Time, float32](dates, func(date time.Time) float32 {
return float32(utils.MustInt(cache, getBillingFormat(getFormat(date)))) / 100.
}),
}
}
func GetErrorData(cache *redis.Client) ErrorChartForm {
dates := getDays(7)
return ErrorChartForm{
Date: getDates(dates),
Value: utils.Each[time.Time, int64](dates, func(date time.Time) int64 {
return utils.MustInt(cache, getErrorFormat(getFormat(date)))
}),
}
}
func GetUserTypeData(db *sql.DB) (UserTypeForm, error) {
var form UserTypeForm
// get total users
if err := globals.QueryRowDb(db, `
SELECT COUNT(*) FROM auth
`).Scan(&form.Total); err != nil {
return form, err
}
// get subscription users count (current subscription)
// level 1: basic plan, level 2: standard plan, level 3: pro plan
if err := globals.QueryRowDb(db, `
SELECT
(SELECT COUNT(*) FROM subscription WHERE level = 1 AND expired_at > NOW()),
(SELECT COUNT(*) FROM subscription WHERE level = 2 AND expired_at > NOW()),
(SELECT COUNT(*) FROM subscription WHERE level = 3 AND expired_at > NOW())
`).Scan(&form.BasicPlan, &form.StandardPlan, &form.ProPlan); err != nil {
return form, err
}
// get normal users count (no subscription in `subscription` table and `quota` + `used` < initial quota in `quota` table)
initialQuota := channel.SystemInstance.GetInitialQuota()
if err := globals.QueryRowDb(db, `
SELECT COUNT(*) FROM auth
WHERE id NOT IN (SELECT user_id FROM subscription WHERE total_month > 0)
AND id IN (SELECT user_id FROM quota WHERE quota + used <= ?)
`, initialQuota).Scan(&form.Normal); err != nil {
return form, err
}
form.ApiPaid = form.Total - form.Normal - form.BasicPlan - form.StandardPlan - form.ProPlan
return form, nil
}
================================================
FILE: admin/analysis/format.go
================================================
package analysis
import (
"fmt"
"time"
)
func getMonth() string {
date := time.Now()
return date.Format("2006-01")
}
func getLastMonth() string {
date := time.Now().AddDate(0, -1, 0)
return date.Format("2006-01")
}
func getDay() string {
date := time.Now()
return date.Format("2006-01-02")
}
func getLastDay() string {
date := time.Now().AddDate(0, 0, -1)
return date.Format("2006-01-02")
}
func getDays(n int) []time.Time {
current := time.Now()
var days []time.Time
for i := n; i > 0; i-- {
days = append(days, current.AddDate(0, 0, -i+1))
}
return days
}
func getErrorFormat(t string) string {
return fmt.Sprintf("nio:err-analysis-%s", t)
}
func getBillingFormat(t string) string {
return fmt.Sprintf("nio:billing-analysis-%s", t)
}
func getMonthBillingFormat(t string) string {
return fmt.Sprintf("nio:billing-analysis-%s", t)
}
func getRequestFormat(t string) string {
return fmt.Sprintf("nio:request-analysis-%s", t)
}
func getModelFormat(t string, model string) string {
return fmt.Sprintf("nio:model-analysis-%s-%s", model, t)
}
func getTpmFormat(t string, user string) string {
return fmt.Sprintf("nio:tpm-analysis-%s-%s", user, t)
}
func getRpmFormat(t string, user string) string {
return fmt.Sprintf("nio:rpm-analysis-%s-%s", user, t)
}
================================================
FILE: admin/analysis/reflect.go
================================================
package analysis
import "reflect"
var _ = reflect.TypeOf(UserTypeForm{})
var _ = reflect.TypeOf(ModelData{})
var _ = reflect.TypeOf(ModelChartForm{})
var _ = reflect.TypeOf(RequestChartForm{})
var _ = reflect.TypeOf(BillingChartForm{})
var _ = reflect.TypeOf(ErrorChartForm{})
================================================
FILE: admin/analysis/statistic.go
================================================
package analysis
import (
"chat/adapter"
"chat/connection"
"chat/utils"
"time"
"github.com/go-redis/redis/v8"
)
func IncrErrorRequest(cache *redis.Client) {
utils.IncrOnce(cache, getErrorFormat(getDay()), time.Hour*24*7*2)
}
func IncrBillingRequest(cache *redis.Client, amount int64, isAdmin bool) {
if isAdmin {
// do not count billing for admin user
return
}
utils.IncrWithExpire(cache, getBillingFormat(getDay()), amount, time.Hour*24*90) // 90 days
utils.IncrWithExpire(cache, getMonthBillingFormat(getMonth()), amount, time.Hour*24*90) // 90 days
}
func IncrRequest(cache *redis.Client) {
utils.IncrOnce(cache, getRequestFormat(getDay()), time.Hour*24*7*2)
}
func IncrModelRequest(cache *redis.Client, model string, tokens int64) {
utils.IncrWithExpire(cache, getModelFormat(getDay(), model), tokens, time.Hour*24*7*2)
}
func AnalyseRequest(model string, user string, buffer *utils.Buffer, err error) {
instance := connection.Cache
if adapter.IsAvailableError(err) {
IncrErrorRequest(instance)
return
}
token := int64(buffer.CountInputToken() + buffer.CountOutputToken(false))
IncrRequest(instance)
IncrModelRequest(instance, model, token)
if buffer != nil {
IncrRpm(instance, user, 1)
IncrTpm(instance, user, token)
// add rpm/tpm to root
IncrRpm(instance, "root", 1)
IncrTpm(instance, "root", token)
}
}
func IncrTpm(cache *redis.Client, user string, n int64) {
utils.IncrWithExpire(cache, getTpmFormat(getMinuteFormat(time.Now()), user), n, time.Minute*5)
}
func IncrRpm(cache *redis.Client, user string, n int64) {
utils.IncrWithExpire(cache, getRpmFormat(getMinuteFormat(time.Now()), user), n, time.Minute*5)
}
================================================
FILE: admin/analysis/types.go
================================================
package analysis
type ModelData struct {
Model string `json:"model"`
Data []int64 `json:"data"`
}
type ModelChartForm struct {
Date []string `json:"date"`
Value []ModelData `json:"value"`
}
type RequestChartForm struct {
Date []string `json:"date"`
Value []int64 `json:"value"`
}
type BillingChartForm struct {
Date []string `json:"date"`
Value []float32 `json:"value"`
}
type ErrorChartForm struct {
Date []string `json:"date"`
Value []int64 `json:"value"`
}
================================================
FILE: admin/controller.go
================================================
package admin
import (
"chat/admin/analysis"
"chat/utils"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
)
type GenerateInvitationForm struct {
Type string `json:"type"`
Quota float32 `json:"quota"`
Number int `json:"number"`
}
type DeleteInvitationForm struct {
Code string `json:"code"`
}
type GenerateRedeemForm struct {
Quota float32 `json:"quota"`
Number int `json:"number"`
}
type PasswordMigrationForm struct {
Id int64 `json:"id"`
Password string `json:"password"`
}
type EmailMigrationForm struct {
Id int64 `json:"id"`
Email string `json:"email"`
}
type SetAdminForm struct {
Id int64 `json:"id"`
Admin bool `json:"admin"`
}
type BanForm struct {
Id int64 `json:"id"`
Ban bool `json:"ban"`
}
type QuotaOperationForm struct {
Id int64 `json:"id" binding:"required"`
Quota *float32 `json:"quota" binding:"required"`
Override bool `json:"override"`
}
type SubscriptionOperationForm struct {
Id int64 `json:"id" binding:"required"`
Expired string `json:"expired" binding:"required"`
}
type SubscriptionLevelForm struct {
Id int64 `json:"id" binding:"required"`
Level *int64 `json:"level" binding:"required"`
}
type ReleaseUsageForm struct {
Id int64 `json:"id" binding:"required"`
}
type UpdateRootPasswordForm struct {
Password string `json:"password" binding:"required"`
}
func UpdateMarketAPI(c *gin.Context) {
var form MarketModelList
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"error": err.Error(),
})
return
}
err := MarketInstance.SetModels(form)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": true,
})
}
func InfoAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)
cache := utils.GetCacheFromContext(c)
c.JSON(http.StatusOK, InfoForm{
OnlineChats: utils.GetConns(),
SubscriptionCount: analysis.GetSubscriptionUsers(db),
BillingToday: analysis.GetBillingToday(cache),
BillingMonth: analysis.GetBillingMonth(cache),
BillingYesterday: analysis.GetBillingYesterday(cache),
BillingLastMonth: analysis.GetBillingLastMonth(cache),
})
}
func ModelAnalysisAPI(c *gin.Context) {
cache := utils.GetCacheFromContext(c)
c.JSON(http.StatusOK, analysis.GetSortedModelData(cache))
}
func RequestAnalysisAPI(c *gin.Context) {
cache := utils.GetCacheFromContext(c)
c.JSON(http.StatusOK, analysis.GetRequestData(cache))
}
func BillingAnalysisAPI(c *gin.Context) {
cache := utils.GetCacheFromContext(c)
c.JSON(http.StatusOK, analysis.GetBillingData(cache))
}
func ErrorAnalysisAPI(c *gin.Context) {
cache := utils.GetCacheFromContext(c)
c.JSON(http.StatusOK, analysis.GetErrorData(cache))
}
func UserTypeAnalysisAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)
if form, err := analysis.GetUserTypeData(db); err != nil {
c.JSON(http.StatusOK, &analysis.UserTypeForm{})
} else {
c.JSON(http.StatusOK, form)
}
}
func RedeemListAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)
page, _ := strconv.Atoi(c.Query("page"))
c.JSON(http.StatusOK, GetRedeemData(db, int64(page)))
}
func DeleteRedeemAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)
var form DeleteInvitationForm
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"error": err.Error(),
})
return
}
err := DeleteRedeemCode(db, form.Code)
c.JSON(http.StatusOK, gin.H{
"status": err == nil,
"error": err,
})
}
func InvitationPaginationAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)
page, _ := strconv.Atoi(c.Query("page"))
c.JSON(http.StatusOK, GetInvitationPagination(db, int64(page)))
}
func DeleteInvitationAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)
var form DeleteInvitationForm
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"error": err.Error(),
})
return
}
err := DeleteInvitationCode(db, form.Code)
c.JSON(http.StatusOK, gin.H{
"status": err == nil,
"error": err,
})
}
func GenerateInvitationAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)
var form GenerateInvitationForm
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, GenerateInvitations(db, form.Number, form.Quota, form.Type))
}
func GenerateRedeemAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)
var form GenerateRedeemForm
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, GenerateRedeemCodes(db, form.Number, form.Quota))
}
func UserPaginationAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)
page, _ := strconv.Atoi(c.Query("page"))
search := strings.TrimSpace(c.Query("search"))
c.JSON(http.StatusOK, getUsersForm(db, int64(page), search))
}
func UpdatePasswordAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)
cache := utils.GetCacheFromContext(c)
var form PasswordMigrationForm
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"error": err.Error(),
})
return
}
err := passwordMigration(db, cache, form.Id, form.Password)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": true,
})
}
func UpdateEmailAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)
var form EmailMigrationForm
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}
err := emailMigration(db, form.Id, form.Email)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": true,
})
}
func SetAdminAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)
var form SetAdminForm
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}
err := setAdmin(db, form.Id, form.Admin)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": true,
})
}
func BanAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)
var form BanForm
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}
err := banUser(db, form.Id, form.Ban)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": true,
})
}
func UserQuotaAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)
var form QuotaOperationForm
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}
err := quotaMigration(db, form.Id, *form.Quota, form.Override)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": true,
})
}
func UserSubscriptionAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)
var form SubscriptionOperationForm
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}
// convert to time
if _, err := time.Parse("2006-01-02 15:04:05", form.Expired); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}
if err := subscriptionMigration(db, form.Id, form.Expired); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": true,
})
}
func SubscriptionLevelAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)
var form SubscriptionLevelForm
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}
err := subscriptionLevelMigration(db, form.Id, *form.Level)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": true,
})
}
func ReleaseUsageAPI(c *gin.Context) {
db := utils.GetDBFromContext(c)
cache := utils.GetCacheFromContext(c)
var form ReleaseUsageForm
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}
err := releaseUsage(db, cache, form.Id)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": true,
})
}
func UpdateRootPasswordAPI(c *gin.Context) {
var form UpdateRootPasswordForm
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"error": err.Error(),
})
return
}
db := utils.GetDBFromContext(c)
cache := utils.GetCacheFromContext(c)
err := UpdateRootPassword(db, cache, form.Password)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": true,
})
}
func ListLoggerAPI(c *gin.Context) {
c.JSON(http.StatusOK, ListLogs())
}
func DownloadLoggerAPI(c *gin.Context) {
path := c.Query("path")
getBlobFile(c, path)
}
func DeleteLoggerAPI(c *gin.Context) {
path := c.Query("path")
if err := deleteLogFile(path); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": true,
})
}
func ConsoleLoggerAPI(c *gin.Context) {
n := utils.ParseInt(c.Query("n"))
content := getLatestLogs(n)
c.JSON(http.StatusOK, gin.H{
"status": true,
"content": content,
})
}
================================================
FILE: admin/format.go
================================================
package admin
import (
"fmt"
"time"
)
func getMonth() string {
date := time.Now()
return date.Format("2006-01")
}
func getDay() string {
date := time.Now()
return date.Format("2006-01-02")
}
func getDays(n int) []time.Time {
current := time.Now()
var days []time.Time
for i := n; i > 0; i-- {
days = append(days, current.AddDate(0, 0, -i+1))
}
return days
}
func getErrorFormat(t string) string {
return fmt.Sprintf("nio:err-analysis-%s", t)
}
func getBillingFormat(t string) string {
return fmt.Sprintf("nio:billing-analysis-%s", t)
}
func getMonthBillingFormat(t string) string {
return fmt.Sprintf("nio:billing-analysis-%s", t)
}
func getRequestFormat(t string) string {
return fmt.Sprintf("nio:request-analysis-%s", t)
}
func getModelFormat(t string, model string) string {
return fmt.Sprintf("nio:model-analysis-%s-%s", model, t)
}
================================================
FILE: admin/instance.go
================================================
package admin
var MarketInstance *Market
func InitInstance() {
MarketInstance = NewMarket()
}
================================================
FILE: admin/invitation.go
================================================
package admin
import (
"chat/globals"
"chat/utils"
"database/sql"
"errors"
"fmt"
"math"
"strings"
)
func GetInvitationPagination(db *sql.DB, page int64) PaginationForm {
var invitations []interface{}
var total int64
if err := globals.QueryRowDb(db, `
SELECT COUNT(*) FROM invitation
`).Scan(&total); err != nil {
return PaginationForm{
Status: false,
Message: err.Error(),
}
}
// get used_user from auth table by `user_id`
rows, err := globals.QueryDb(db, `
SELECT invitation.code, invitation.quota, invitation.type, invitation.used,
invitation.created_at, invitation.updated_at,
COALESCE(auth.username, '-') as username
FROM invitation
LEFT JOIN auth ON auth.id = invitation.used_id
ORDER BY invitation.id DESC LIMIT ? OFFSET ?
`, pagination, page*pagination)
if err != nil {
return PaginationForm{
Status: false,
Message: err.Error(),
}
}
for rows.Next() {
var invitation InvitationData
var createdAt []uint8
var updatedAt []uint8
if err := rows.Scan(&invitation.Code, &invitation.Quota, &invitation.Type, &invitation.Used, &createdAt, &updatedAt, &invitation.Username); err != nil {
return PaginationForm{
Status: false,
Message: err.Error(),
}
}
invitation.CreatedAt = utils.ConvertTime(createdAt).Format("2006-01-02 15:04:05")
invitation.UpdatedAt = utils.ConvertTime(updatedAt).Format("2006-01-02 15:04:05")
invitations = append(invitations, invitation)
}
return PaginationForm{
Status: true,
Total: int(math.Ceil(float64(total) / float64(pagination))),
Data: invitations,
}
}
func DeleteInvitationCode(db *sql.DB, code string) error {
_, err := globals.ExecDb(db, `
DELETE FROM invitation WHERE code = ?
`, code)
return err
}
func NewInvitationCode(db *sql.DB, code string, quota float32, t string) error {
_, err := globals.ExecDb(db, `
INSERT INTO invitation (code, quota, type)
VALUES (?, ?, ?)
`, code, quota, t)
return err
}
func GenerateInvitations(db *sql.DB, num int, quota float32, t string) InvitationGenerateResponse {
arr := make([]string, 0)
idx := 0
retry := 0
for idx < num {
code := fmt.Sprintf("%s-%s", t, utils.GenerateChar(24))
if err := NewInvitationCode(db, code, quota, t); err != nil {
// ignore duplicate code
if errors.Is(err, sql.ErrNoRows) {
continue
}
if retry < 100 && strings.Contains(err.Error(), "Duplicate entry") {
retry++
continue
}
retry = 0
return InvitationGenerateResponse{
Status: false,
Message: err.Error(),
}
}
arr = append(arr, code)
idx++
}
return InvitationGenerateResponse{
Status: true,
Data: arr,
}
}
================================================
FILE: admin/logger.go
================================================
package admin
import (
"chat/globals"
"chat/utils"
"fmt"
"github.com/gin-gonic/gin"
"strings"
)
type LogFile struct {
Path string `json:"path"`
Size int64 `json:"size"`
}
func ListLogs() []LogFile {
return utils.Each(utils.Walk("logs"), func(path string) LogFile {
return LogFile{
Path: strings.TrimLeft(path, "logs/"),
Size: utils.GetFileSize(path),
}
})
}
func getLogPath(path string) string {
return fmt.Sprintf("logs/%s", path)
}
func getBlobFile(c *gin.Context, path string) {
c.File(getLogPath(path))
}
func deleteLogFile(path string) error {
return utils.DeleteFile(getLogPath(path))
}
func getLatestLogs(n int) string {
if n <= 0 {
n = 100
}
content, err := utils.ReadFileLatestLines(getLogPath(globals.DefaultLoggerFile), n)
if err != nil {
return fmt.Sprintf("read error: %s", err.Error())
}
return content
}
================================================
FILE: admin/market.go
================================================
package admin
import (
"chat/globals"
"chat/utils"
"fmt"
"github.com/spf13/viper"
)
type ModelTag []string
type MarketModel struct {
Id string `json:"id" mapstructure:"id" required:"true"`
Name string `json:"name" mapstructure:"name" required:"true"`
Description string `json:"description" mapstructure:"description"`
Default bool `json:"default" mapstructure:"default"`
HighContext bool `json:"high_context" mapstructure:"highcontext"`
Avatar string `json:"avatar" mapstructure:"avatar"`
Tag ModelTag `json:"tag" mapstructure:"tag"`
}
type MarketModelList []MarketModel
type Market struct {
Models MarketModelList `json:"models" mapstructure:"models"`
}
func NewMarket() *Market {
var models MarketModelList
if err := viper.UnmarshalKey("market", &models); err != nil {
globals.Warn(fmt.Sprintf("[market] read config error: %s, use default config", err.Error()))
models = MarketModelList{}
}
return &Market{
Models: models,
}
}
func (m *Market) GetModels() MarketModelList {
return m.Models
}
func (m *Market) GetModel(id string) *MarketModel {
for _, model := range m.Models {
if model.Id == id {
return &model
}
}
return nil
}
func (m *Market) SaveConfig() error {
return utils.SaveConfig("market", m.Models)
}
func (m *Market) SetModels(models MarketModelList) error {
m.Models = models
return m.SaveConfig()
}
================================================
FILE: admin/redeem.go
================================================
package admin
import (
"chat/globals"
"chat/utils"
"database/sql"
"fmt"
"math"
"strings"
)
func GetRedeemData(db *sql.DB, page int64) PaginationForm {
var data []interface{}
var total int64
if err := globals.QueryRowDb(db, `
SELECT COUNT(*) FROM redeem
`).Scan(&total); err != nil {
return PaginationForm{
Status: false,
Message: err.Error(),
}
}
rows, err := globals.QueryDb(db, `
SELECT code, quota, used, created_at, updated_at
FROM redeem
ORDER BY id DESC LIMIT ? OFFSET ?
`, pagination, page*pagination)
if err != nil {
return PaginationForm{
Status: false,
Message: err.Error(),
}
}
for rows.Next() {
var redeem RedeemData
var createdAt []uint8
var updatedAt []uint8
if err := rows.Scan(&redeem.Code, &redeem.Quota, &redeem.Used, &createdAt, &updatedAt); err != nil {
return PaginationForm{
Status: false,
Message: err.Error(),
}
}
redeem.CreatedAt = utils.ConvertTime(createdAt).Format("2006-01-02 15:04:05")
redeem.UpdatedAt = utils.ConvertTime(updatedAt).Format("2006-01-02 15:04:05")
data = append(data, redeem)
}
return PaginationForm{
Status: true,
Total: int(math.Ceil(float64(total) / float64(pagination))),
Data: data,
}
}
func DeleteRedeemCode(db *sql.DB, code string) error {
_, err := globals.ExecDb(db, `
DELETE FROM redeem WHERE code = ?
`, code)
return err
}
func GenerateRedeemCodes(db *sql.DB, num int, quota float32) RedeemGenerateResponse {
arr := make([]string, 0)
idx := 0
for idx < num {
code, err := CreateRedeemCode(db, quota)
if err != nil {
return RedeemGenerateResponse{
Status: false,
Message: err.Error(),
}
}
arr = append(arr, code)
idx++
}
return RedeemGenerateResponse{
Status: true,
Data: arr,
}
}
func CreateRedeemCode(db *sql.DB, quota float32) (string, error) {
code := fmt.Sprintf("nio-%s", utils.GenerateChar(32))
_, err := globals.ExecDb(db, `
INSERT INTO redeem (code, quota) VALUES (?, ?)
`, code, quota)
if err != nil && strings.Contains(err.Error(), "Duplicate entry") {
// code name is duplicate
return CreateRedeemCode(db, quota)
}
return code, err
}
================================================
FILE: admin/router.go
================================================
package admin
import (
"chat/addition/web"
"chat/channel"
"github.com/gin-gonic/gin"
)
func Register(app *gin.RouterGroup) {
channel.Register(app)
app.GET("/admin/config/test/search", web.TestSearch)
app.GET("/admin/analytics/info", InfoAPI)
app.GET("/admin/analytics/model", ModelAnalysisAPI)
app.GET("/admin/analytics/request", RequestAnalysisAPI)
app.GET("/admin/analytics/billing", BillingAnalysisAPI)
app.GET("/admin/analytics/error", ErrorAnalysisAPI)
app.GET("/admin/analytics/user", UserTypeAnalysisAPI)
app.GET("/admin/invitation/list", InvitationPaginationAPI)
app.POST("/admin/invitation/generate", GenerateInvitationAPI)
app.POST("/admin/invitation/delete", DeleteInvitationAPI)
app.GET("/admin/redeem/list", RedeemListAPI)
app.POST("/admin/redeem/generate", GenerateRedeemAPI)
app.POST("/admin/redeem/delete", DeleteRedeemAPI)
app.GET("/admin/user/list", UserPaginationAPI)
app.POST("/admin/user/quota", UserQuotaAPI)
app.POST("/admin/user/subscription", UserSubscriptionAPI)
app.POST("/admin/user/level", SubscriptionLevelAPI)
app.POST("/admin/user/release", ReleaseUsageAPI)
app.POST("/admin/user/password", UpdatePasswordAPI)
app.POST("/admin/user/email", UpdateEmailAPI)
app.POST("/admin/user/ban", BanAPI)
app.POST("/admin/user/admin", SetAdminAPI)
app.POST("/admin/user/root", UpdateRootPasswordAPI)
app.POST("/admin/market/update", UpdateMarketAPI)
app.GET("/admin/logger/list", ListLoggerAPI)
app.GET("/admin/logger/download", DownloadLoggerAPI)
app.GET("/admin/logger/console", ConsoleLoggerAPI)
app.POST("/admin/logger/delete", DeleteLoggerAPI)
}
================================================
FILE: admin/statistic.go
================================================
package admin
import (
"chat/adapter"
"chat/connection"
"chat/utils"
"time"
"github.com/go-redis/redis/v8"
)
func IncrErrorRequest(cache *redis.Client) {
utils.IncrOnce(cache, getErrorFormat(getDay()), time.Hour*24*7*2)
}
func IncrBillingRequest(cache *redis.Client, amount int64) {
utils.IncrWithExpire(cache, getBillingFormat(getDay()), amount, time.Hour*24*30*2)
utils.IncrWithExpire(cache, getMonthBillingFormat(getMonth()), amount, time.Hour*24*30*2)
}
func IncrRequest(cache *redis.Client) {
utils.IncrOnce(cache, getRequestFormat(getDay()), time.Hour*24*7*2)
}
func IncrModelRequest(cache *redis.Client, model string, tokens int64) {
utils.IncrWithExpire(cache, getModelFormat(getDay(), model), tokens, time.Hour*24*7*2)
}
func AnalyseRequest(model string, buffer *utils.Buffer, err error) {
instance := connection.Cache
if adapter.IsAvailableError(err) {
IncrErrorRequest(instance)
return
}
IncrRequest(instance)
IncrModelRequest(instance, model, int64(buffer.CountToken()))
}
================================================
FILE: admin/types.go
================================================
package admin
var pagination int64 = 10
type InfoForm struct {
BillingToday float32 `json:"billing_today"`
BillingMonth float32 `json:"billing_month"`
BillingYesterday float32 `json:"billing_yesterday"`
BillingLastMonth float32 `json:"billing_last_month"`
SubscriptionCount int64 `json:"subscription_count"`
OnlineChats int64 `json:"online_chats"`
}
type ModelData struct {
Model string `json:"model"`
Data []int64 `json:"data"`
}
type ModelChartForm struct {
Date []string `json:"date"`
Value []ModelData `json:"value"`
}
type RequestChartForm struct {
Date []string `json:"date"`
Value []int64 `json:"value"`
}
type BillingChartForm struct {
Date []string `json:"date"`
Value []float32 `json:"value"`
}
type ErrorChartForm struct {
Date []string `json:"date"`
Value []int64 `json:"value"`
}
type PaginationForm struct {
Status bool `json:"status"`
Total int `json:"total"`
Data []interface{} `json:"data"`
Message string `json:"message"`
}
type InvitationData struct {
Code string `json:"code"`
Quota float32 `json:"quota"`
Type string `json:"type"`
Used bool `json:"used"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Username string `json:"username"`
}
type RedeemData struct {
Code string `json:"code"`
Quota float32 `json:"quota"`
Used bool `json:"used"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type InvitationGenerateResponse struct {
Status bool `json:"status"`
Message string `json:"message"`
Data []string `json:"data"`
}
type RedeemGenerateResponse struct {
Status bool `json:"status"`
Message string `json:"message"`
Data []string `json:"data"`
}
type UserData struct {
Id int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
IsAdmin bool `json:"is_admin"`
Quota float32 `json:"quota"`
UsedQuota float32 `json:"used_quota"`
ExpiredAt string `json:"expired_at"`
IsSubscribed bool `json:"is_subscribed"`
TotalMonth int64 `json:"total_month"`
Enterprise bool `json:"enterprise"`
Level int `json:"level"`
IsBanned bool `json:"is_banned"`
}
================================================
FILE: admin/user.go
================================================
package admin
import (
"chat/channel"
"chat/globals"
"chat/utils"
"context"
"database/sql"
"fmt"
"math"
"strings"
"time"
"github.com/go-redis/redis/v8"
)
// AuthLike is to solve the problem of import cycle
type AuthLike struct {
ID int64 `json:"id"`
}
func (a *AuthLike) GetID(_ *sql.DB) int64 {
return a.ID
}
func (a *AuthLike) HitID() int64 {
return a.ID
}
func getUsersForm(db *sql.DB, page int64, search string) PaginationForm {
// if search is empty, then search all users
var users []interface{}
var total int64
if err := globals.QueryRowDb(db, `
SELECT COUNT(*) FROM auth
WHERE username LIKE ?
`, "%"+search+"%").Scan(&total); err != nil {
return PaginationForm{
Status: false,
Message: err.Error(),
}
}
rows, err := globals.QueryDb(db, `
SELECT
auth.id, auth.username, auth.email, auth.is_admin,
quota.quota, quota.used,
subscription.expired_at, subscription.total_month, subscription.enterprise, subscription.level,
auth.is_banned
FROM auth
LEFT JOIN quota ON quota.user_id = auth.id
LEFT JOIN subscription ON subscription.user_id = auth.id
WHERE auth.username LIKE ?
ORDER BY auth.id LIMIT ? OFFSET ?
`, "%"+search+"%", pagination, page*pagination)
if err != nil {
return PaginationForm{
Status: false,
Message: err.Error(),
}
}
for rows.Next() {
var user UserData
var (
email sql.NullString
expired []uint8
quota sql.NullFloat64
usedQuota sql.NullFloat64
totalMonth sql.NullInt64
isEnterprise sql.NullBool
subscriptionLevel sql.NullInt64
isBanned sql.NullBool
)
if err := rows.Scan(&user.Id, &user.Username, &email, &user.IsAdmin, "a, &usedQuota, &expired, &totalMonth, &isEnterprise, &subscriptionLevel, &isBanned); err != nil {
return PaginationForm{
Status: false,
Message: err.Error(),
}
}
if email.Valid {
user.Email = email.String
}
if quota.Valid {
user.Quota = float32(quota.Float64)
}
if usedQuota.Valid {
user.UsedQuota = float32(usedQuota.Float64)
}
if totalMonth.Valid {
user.TotalMonth = totalMonth.Int64
}
if subscriptionLevel.Valid {
user.Level = int(subscriptionLevel.Int64)
}
stamp := utils.ConvertTime(expired)
if stamp != nil {
user.IsSubscribed = stamp.After(time.Now())
user.ExpiredAt = stamp.Format("2006-01-02 15:04:05")
}
user.Enterprise = isEnterprise.Valid && isEnterprise.Bool
user.IsBanned = isBanned.Valid && isBanned.Bool
users = append(users, user)
}
return PaginationForm{
Status: true,
Total: int(math.Ceil(float64(total) / float64(pagination))),
Data: users,
}
}
// clearUserCache clears all cache keys starting with nio:user:
func clearUserCache(cache *redis.Client) error {
ctx := context.Background()
iter := cache.Scan(ctx, 0, "nio:user:*", 100).Iterator()
for iter.Next(ctx) {
if err := cache.Del(ctx, iter.Val()).Err(); err != nil {
return fmt.Errorf("failed to delete cache key %s: %v", iter.Val(), err)
}
}
return iter.Err()
}
func passwordMigration(db *sql.DB, cache *redis.Client, id int64, password string) error {
password = strings.TrimSpace(password)
if len(password) < 6 || len(password) > 36 {
return fmt.Errorf("password length must be between 6 and 36")
}
hash_passwd := utils.Sha2Encrypt(password)
// Update password in database
_, err := globals.ExecDb(db, `
UPDATE auth SET password = ? WHERE id = ?
`, hash_passwd, id)
if err != nil {
return err
}
// Clear all user related cache
if err := clearUserCache(cache); err != nil {
return fmt.Errorf("failed to clear user cache: %v", err)
}
return nil
}
func emailMigration(db *sql.DB, id int64, email string) error {
_, err := globals.ExecDb(db, `
UPDATE auth SET email = ? WHERE id = ?
`, email, id)
return err
}
func setAdmin(db *sql.DB, id int64, isAdmin bool) error {
_, err := globals.ExecDb(db, `
UPDATE auth SET is_admin = ? WHERE id = ?
`, isAdmin, id)
return err
}
func banUser(db *sql.DB, id int64, isBanned bool) error {
_, err := globals.ExecDb(db, `
UPDATE auth SET is_banned = ? WHERE id = ?
`, isBanned, id)
return err
}
func quotaMigration(db *sql.DB, id int64, quota float32, override bool) error {
// if quota is negative, then decrease quota
// if quota is positive, then increase quota
if override {
_, err := globals.ExecDb(db, `
INSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE quota = ?
`, id, quota, 0., quota)
return err
}
_, err := globals.ExecDb(db, `
INSERT INTO quota (user_id, quota, used) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE quota = quota + ?
`, id, quota, 0., quota)
return err
}
func subscriptionMigration(db *sql.DB, id int64, expired string) error {
_, err := globals.ExecDb(db, `
INSERT INTO subscription (user_id, expired_at) VALUES (?, ?)
ON DUPLICATE KEY UPDATE expired_at = ?
`, id, expired, expired)
return err
}
func subscriptionLevelMigration(db *sql.DB, id int64, level int64) error {
if level < 0 || level > 3 {
return fmt.Errorf("invalid subscription level")
}
_, err := globals.ExecDb(db, `
INSERT INTO subscription (user_id, level) VALUES (?, ?)
ON DUPLICATE KEY UPDATE level = ?
`, id, level, level)
return err
}
func releaseUsage(db *sql.DB, cache *redis.Client, id int64) error {
var level sql.NullInt64
if err := globals.QueryRowDb(db, `
SELECT level FROM subscription WHERE user_id = ?
`, id).Scan(&level); err != nil {
return err
}
if !level.Valid || level.Int64 == 0 {
return fmt.Errorf("user is not subscribed")
}
u := &AuthLike{ID: id}
plan := channel.PlanInstance.GetPlan(int(level.Int64))
if !plan.ReleaseAll(u, cache) {
return fmt.Errorf("cannot release usage")
}
return nil
}
func UpdateRootPassword(db *sql.DB, cache *redis.Client, password string) error {
password = strings.TrimSpace(password)
if len(password) < 6 || len(password) > 36 {
return fmt.Errorf("password length must be between 6 and 36")
}
if _, err := globals.ExecDb(db, `
UPDATE auth SET password = ? WHERE username = 'root'
`, utils.Sha2Encrypt(password)); err != nil {
return err
}
// Clear all user related cache
if err := clearUserCache(cache); err != nil {
return fmt.Errorf("failed to clear user cache: %v", err)
}
return nil
}
================================================
FILE: app/.eslintrc.cjs
================================================
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
================================================
FILE: app/.gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
dev-dist
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Libre
db
================================================
FILE: app/.prettierrc.json
================================================
{}
================================================
FILE: app/components.json
================================================
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "src/components",
"utils": "@/components/ui/lib/utils"
}
}
================================================
FILE: app/index.html
================================================
CoAI.Dev
You need to enable JavaScript to run this app.
================================================
FILE: app/package.json
================================================
{
"name": "coai",
"private": false,
"version": "2.6.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && cross-env NODE_OPTIONS=--max_old_space_size=4096 vite build --mode production",
"fast-build": "cross-env NODE_OPTIONS=--max_old_space_size=4096 vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"prettier": "prettier --write \"src/**/*.tsx\" \"src/**/*.ts\"",
"preview": "vite preview"
},
"dependencies": {
"@fontsource-variable/inter": "^5.1.0",
"@fontsource-variable/jetbrains-mono": "^5.1.1",
"@headlessui/react": "^1.7.18",
"@headlessui/tailwindcss": "^0.2.0",
"@lobehub/icons": "1.49.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-context-menu": "^2.1.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-toggle-group": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.6",
"@reduxjs/toolkit": "^1.9.5",
"@tanem/react-nprogress": "^5.0.51",
"@tremor/react": "^3.14.0",
"@types/crypto-js": "^4.2.2",
"axios": "^1.5.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"crypto-js": "^4.2.0",
"date-fns": "^3.6.0",
"emoji-picker-react": "^4.7.18",
"framer-motion": "^11.3.28",
"html-to-image": "^1.11.11",
"i18next": "^23.4.6",
"localforage": "^1.10.0",
"lucide-react": "^0.483.0",
"match-sorter": "^6.3.1",
"mermaid": "^10.9.0",
"next-themes": "^0.2.1",
"normalize.css": "^8.0.1",
"qrcode.react": "^4.2.0",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-day-picker": "^8.10.0",
"react-dom": "^18.2.0",
"react-ga4": "^2.1.0",
"react-i18next": "^13.2.2",
"react-intersection-observer": "^9.13.1",
"react-markdown": "^8.0.7",
"react-redux": "^8.1.2",
"react-router-dom": "^6.17.0",
"react-syntax-highlighter": "^15.5.0",
"react-virtuoso": "^4.7.10",
"react-window": "^1.8.10",
"rehype-katex": "^6.0.3",
"rehype-raw": "^7.0.0",
"remark-breaks": "^4.0.0",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"sonner": "^1.5.0",
"sort-by": "^1.2.0",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"use-debounce": "^10.0.0",
"vaul": "^0.9.0",
"workbox-window": "^7.0.0"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"@tauri-apps/cli": "^1.5.6",
"@types/node": "^20.5.9",
"@types/react": "^18.2.15",
"@types/react-beautiful-dnd": "^13.1.8",
"@types/react-dom": "^18.2.7",
"@types/react-syntax-highlighter": "^15.5.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.15",
"cross-env": "^7.0.3",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"less": "^4.2.0",
"less-loader": "^11.1.3",
"postcss": "^8.4.29",
"prettier": "^3.0.3",
"tailwindcss": "^3.3.3",
"typescript": "^5.0.2",
"vite": "^4.4.5",
"vite-plugin-html": "^3.2.0"
}
}
================================================
FILE: app/postcss.config.js
================================================
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
================================================
FILE: app/public/manifest.json
================================================
{
"_1c.7075dba8.js": {
"file": "assets/1c.7075dba8.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_OperationAction.5b1f951a.js": {
"file": "assets/OperationAction.5b1f951a.js",
"imports": [
"index.html"
]
},
"_Paragraph.4ca577cf.js": {
"file": "assets/Paragraph.4ca577cf.js",
"imports": [
"index.html"
]
},
"_Tableau10.1b767f5e.js": {
"file": "assets/Tableau10.1b767f5e.js"
},
"_Tracker.20c1771e.js": {
"file": "assets/Tracker.20c1771e.js",
"imports": [
"index.html",
"__isIndex.00b9330c.js",
"_tiny-invariant.dd7d57d2.js",
"_path.53f90ab3.js",
"_linear.1519afab.js",
"_time.f5c88166.js",
"_init.a5b10ee5.js",
"_band.6ffec690.js",
"_ordinal.93cdc51b.js",
"_array.9f3ba611.js",
"_line.c9b6b09c.js"
]
},
"__isIndex.00b9330c.js": {
"file": "assets/_isIndex.00b9330c.js",
"imports": [
"index.html"
]
},
"_abnf.9bf3a7c1.js": {
"file": "assets/abnf.9bf3a7c1.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_accesslog.68f2b66c.js": {
"file": "assets/accesslog.68f2b66c.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_actionscript.67a1e568.js": {
"file": "assets/actionscript.67a1e568.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_activity.612642ea.js": {
"file": "assets/activity.612642ea.js",
"imports": [
"index.html"
]
},
"_ada.c0e23f60.js": {
"file": "assets/ada.c0e23f60.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_angelscript.410ffc68.js": {
"file": "assets/angelscript.410ffc68.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_apache.006764de.js": {
"file": "assets/apache.006764de.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_applescript.39299b7d.js": {
"file": "assets/applescript.39299b7d.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_arc.a247e62c.js": {
"file": "assets/arc.a247e62c.js",
"imports": [
"_path.53f90ab3.js",
"index.html"
]
},
"_arcade.f590a3cc.js": {
"file": "assets/arcade.f590a3cc.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_arduino.c4c70db8.js": {
"file": "assets/arduino.c4c70db8.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_armasm.50cdbe9a.js": {
"file": "assets/armasm.50cdbe9a.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_array.9f3ba611.js": {
"file": "assets/array.9f3ba611.js"
},
"_arrow-right-left.f4c82cc0.js": {
"file": "assets/arrow-right-left.f4c82cc0.js",
"imports": [
"index.html"
]
},
"_asciidoc.e1e2cbee.js": {
"file": "assets/asciidoc.e1e2cbee.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_aspectj.a696b879.js": {
"file": "assets/aspectj.a696b879.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_asterisk-square.2615ee58.js": {
"file": "assets/asterisk-square.2615ee58.js",
"imports": [
"index.html"
]
},
"_autohotkey.c54238fc.js": {
"file": "assets/autohotkey.c54238fc.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_autoit.25b13806.js": {
"file": "assets/autoit.25b13806.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_avrasm.a69814df.js": {
"file": "assets/avrasm.a69814df.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_awk.9f65b5a4.js": {
"file": "assets/awk.9f65b5a4.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_axapta.fa0aef8d.js": {
"file": "assets/axapta.fa0aef8d.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_band.6ffec690.js": {
"file": "assets/band.6ffec690.js",
"imports": [
"_init.a5b10ee5.js",
"_ordinal.93cdc51b.js"
]
},
"_bash.a8e19ccc.js": {
"file": "assets/bash.a8e19ccc.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_basic.6acc6ac9.js": {
"file": "assets/basic.6acc6ac9.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_bnf.bd11f475.js": {
"file": "assets/bnf.bd11f475.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_book-dashed.798b0c5c.js": {
"file": "assets/book-dashed.798b0c5c.js",
"imports": [
"index.html"
]
},
"_box.6dd688fe.js": {
"file": "assets/box.6dd688fe.js",
"imports": [
"index.html"
]
},
"_brainfuck.db01d938.js": {
"file": "assets/brainfuck.db01d938.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_c-like.af826c24.js": {
"file": "assets/c-like.af826c24.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_c.cc4c2414.js": {
"file": "assets/c.cc4c2414.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_cal.b721a23c.js": {
"file": "assets/cal.b721a23c.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_calendar-clock.1432b482.js": {
"file": "assets/calendar-clock.1432b482.js",
"imports": [
"index.html"
]
},
"_calendar.2f96549a.js": {
"file": "assets/calendar.2f96549a.js",
"imports": [
"index.html"
]
},
"_capnproto.7f37471d.js": {
"file": "assets/capnproto.7f37471d.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_ceylon.f41d8cb9.js": {
"file": "assets/ceylon.f41d8cb9.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_channel.0e442477.js": {
"file": "assets/channel.0e442477.js",
"imports": [
"index.html"
]
},
"_chart.b43e29db.js": {
"file": "assets/chart.b43e29db.js",
"imports": [
"index.html"
]
},
"_clean.2230529c.js": {
"file": "assets/clean.2230529c.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_clock.36b36b21.js": {
"file": "assets/clock.36b36b21.js",
"imports": [
"index.html"
]
},
"_clojure-repl.24369284.js": {
"file": "assets/clojure-repl.24369284.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_clojure.98540e11.js": {
"file": "assets/clojure.98540e11.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_clone.2acf797d.js": {
"file": "assets/clone.2acf797d.js",
"imports": [
"_graph.0ef9c3b7.js"
]
},
"_cmake.687d96e9.js": {
"file": "assets/cmake.687d96e9.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_coffeescript.09ac2f3f.js": {
"file": "assets/coffeescript.09ac2f3f.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_coq.18e8d32c.js": {
"file": "assets/coq.18e8d32c.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_core.b8b80318.js": {
"file": "assets/core.b8b80318.js",
"isDynamicEntry": true
},
"_cos.cfa5f5a1.js": {
"file": "assets/cos.cfa5f5a1.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_cpp.a6b75908.js": {
"file": "assets/cpp.a6b75908.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_createText-6b48ae7d.01af0827.js": {
"file": "assets/createText-6b48ae7d.01af0827.js",
"imports": [
"index.html"
]
},
"_crmsh.929b5790.js": {
"file": "assets/crmsh.929b5790.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_crystal.520aae80.js": {
"file": "assets/crystal.520aae80.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_csharp.676d187e.js": {
"file": "assets/csharp.676d187e.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_csp.27c6fdf5.js": {
"file": "assets/csp.27c6fdf5.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_css.aed2fec3.js": {
"file": "assets/css.aed2fec3.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_d.deee4072.js": {
"file": "assets/d.deee4072.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_dart.86736099.js": {
"file": "assets/dart.86736099.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_delphi.b2e83fb0.js": {
"file": "assets/delphi.b2e83fb0.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_diff.52ac5061.js": {
"file": "assets/diff.52ac5061.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_django.acf6f5d0.js": {
"file": "assets/django.acf6f5d0.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_dns.ae462891.js": {
"file": "assets/dns.ae462891.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_dockerfile.a461ef01.js": {
"file": "assets/dockerfile.a461ef01.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_dos.66ec8774.js": {
"file": "assets/dos.66ec8774.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_download-cloud.04333a4f.js": {
"file": "assets/download-cloud.04333a4f.js",
"imports": [
"index.html"
]
},
"_dsconfig.948d33c9.js": {
"file": "assets/dsconfig.948d33c9.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_dts.ace8285a.js": {
"file": "assets/dts.ace8285a.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_dust.61c6165d.js": {
"file": "assets/dust.61c6165d.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_ebnf.9f47f2bf.js": {
"file": "assets/ebnf.9f47f2bf.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_edges-d32062c0.bb379725.js": {
"file": "assets/edges-d32062c0.bb379725.js",
"imports": [
"index.html",
"_createText-6b48ae7d.01af0827.js",
"_line.c9b6b09c.js"
]
},
"_elixir.87b77a7b.js": {
"file": "assets/elixir.87b77a7b.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_elm.62fc5561.js": {
"file": "assets/elm.62fc5561.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_erb.55b07d6c.js": {
"file": "assets/erb.55b07d6c.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_erlang-repl.323f7160.js": {
"file": "assets/erlang-repl.323f7160.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_erlang.c7e57743.js": {
"file": "assets/erlang.c7e57743.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_excel.4522222f.js": {
"file": "assets/excel.4522222f.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_filter.0ca1fe92.js": {
"file": "assets/filter.0ca1fe92.js",
"imports": [
"index.html"
]
},
"_fix.51ea128a.js": {
"file": "assets/fix.51ea128a.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_flix.c3fcc323.js": {
"file": "assets/flix.c3fcc323.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_flowDb-4b19a42f.0b3ed11f.js": {
"file": "assets/flowDb-4b19a42f.0b3ed11f.js",
"imports": [
"index.html"
]
},
"_fortran.5261c416.js": {
"file": "assets/fortran.5261c416.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_fsharp.11981ee5.js": {
"file": "assets/fsharp.11981ee5.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_gams.49652455.js": {
"file": "assets/gams.49652455.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_gauss.012009e3.js": {
"file": "assets/gauss.012009e3.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_gcode.0c400ff3.js": {
"file": "assets/gcode.0c400ff3.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_gherkin.6750692c.js": {
"file": "assets/gherkin.6750692c.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_gift.b3e4cc82.js": {
"file": "assets/gift.b3e4cc82.js",
"imports": [
"index.html"
]
},
"_glsl.3aa8f677.js": {
"file": "assets/glsl.3aa8f677.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_gml.8a3d692a.js": {
"file": "assets/gml.8a3d692a.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_go.9e6e090c.js": {
"file": "assets/go.9e6e090c.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_golo.0c3cd41a.js": {
"file": "assets/golo.0c3cd41a.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_gradle.9b2be066.js": {
"file": "assets/gradle.9b2be066.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_graph.0ef9c3b7.js": {
"file": "assets/graph.0ef9c3b7.js",
"imports": [
"index.html"
]
},
"_groovy.e5a1e4e3.js": {
"file": "assets/groovy.e5a1e4e3.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_haml.0925c5ed.js": {
"file": "assets/haml.0925c5ed.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_handlebars.a705eec6.js": {
"file": "assets/handlebars.a705eec6.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_haskell.c1c10057.js": {
"file": "assets/haskell.c1c10057.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_haxe.bb1f4576.js": {
"file": "assets/haxe.bb1f4576.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_history.44d149e5.js": {
"file": "assets/history.44d149e5.js",
"imports": [
"index.html"
]
},
"_hook.09e5815b.js": {
"file": "assets/hook.09e5815b.js",
"imports": [
"index.html"
]
},
"_hsp.acbd24bb.js": {
"file": "assets/hsp.acbd24bb.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_htmlbars.ae56fbaf.js": {
"file": "assets/htmlbars.ae56fbaf.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_http.cadf2244.js": {
"file": "assets/http.cadf2244.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_hy.94f04bbe.js": {
"file": "assets/hy.94f04bbe.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_icons.7e9c9414.js": {
"file": "assets/icons.7e9c9414.js",
"imports": [
"index.html"
]
},
"_index-fc10efb0.021331b3.js": {
"file": "assets/index-fc10efb0.021331b3.js",
"imports": [
"_graph.0ef9c3b7.js",
"_layout.991ed0c6.js",
"_clone.2acf797d.js",
"_edges-d32062c0.bb379725.js",
"index.html",
"_createText-6b48ae7d.01af0827.js"
]
},
"_inform7.573ba170.js": {
"file": "assets/inform7.573ba170.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_ini.0ce80bb9.js": {
"file": "assets/ini.0ce80bb9.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_init.a5b10ee5.js": {
"file": "assets/init.a5b10ee5.js"
},
"_irpf90.a8791531.js": {
"file": "assets/irpf90.a8791531.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_isbl.17bd57b9.js": {
"file": "assets/isbl.17bd57b9.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_java.de21cb0c.js": {
"file": "assets/java.de21cb0c.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_javascript.4b296926.js": {
"file": "assets/javascript.4b296926.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_jboss-cli.f0eb5633.js": {
"file": "assets/jboss-cli.f0eb5633.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_json.6e716f3a.js": {
"file": "assets/json.6e716f3a.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_julia-repl.6f5d2fd3.js": {
"file": "assets/julia-repl.6f5d2fd3.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_julia.41a3881b.js": {
"file": "assets/julia.41a3881b.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_kanban-square-dashed.c88dceb2.js": {
"file": "assets/kanban-square-dashed.c88dceb2.js",
"imports": [
"index.html"
]
},
"_kotlin.c59af0ba.js": {
"file": "assets/kotlin.c59af0ba.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_lasso.8a28f498.js": {
"file": "assets/lasso.8a28f498.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_latex.058112d0.js": {
"file": "assets/latex.058112d0.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_layout.991ed0c6.js": {
"file": "assets/layout.991ed0c6.js",
"imports": [
"_graph.0ef9c3b7.js",
"index.html"
]
},
"_ldif.e8f34ce7.js": {
"file": "assets/ldif.e8f34ce7.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_leaf.9ed3302f.js": {
"file": "assets/leaf.9ed3302f.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_less.1e2f29fc.js": {
"file": "assets/less.1e2f29fc.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_line.c9b6b09c.js": {
"file": "assets/line.c9b6b09c.js",
"imports": [
"_array.9f3ba611.js",
"_path.53f90ab3.js",
"index.html"
]
},
"_linear.1519afab.js": {
"file": "assets/linear.1519afab.js",
"imports": [
"index.html",
"_init.a5b10ee5.js"
]
},
"_lisp.ab804084.js": {
"file": "assets/lisp.ab804084.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_livecodeserver.ada7c599.js": {
"file": "assets/livecodeserver.ada7c599.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_livescript.09fbd630.js": {
"file": "assets/livescript.09fbd630.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_llvm.c75c9c59.js": {
"file": "assets/llvm.c75c9c59.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_lsl.21a37c7a.js": {
"file": "assets/lsl.21a37c7a.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_lua.c0a16d09.js": {
"file": "assets/lua.c0a16d09.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_makefile.bb0fcfbd.js": {
"file": "assets/makefile.bb0fcfbd.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_markdown.26dd2c01.js": {
"file": "assets/markdown.26dd2c01.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_market.93d110b3.js": {
"file": "assets/market.93d110b3.js"
},
"_mathematica.2dc23a04.js": {
"file": "assets/mathematica.2dc23a04.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_matlab.e39e79ce.js": {
"file": "assets/matlab.e39e79ce.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_maxima.b42deb33.js": {
"file": "assets/maxima.b42deb33.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_mel.30384c03.js": {
"file": "assets/mel.30384c03.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_mercury.4f44fcae.js": {
"file": "assets/mercury.4f44fcae.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_mipsasm.6d204ed1.js": {
"file": "assets/mipsasm.6d204ed1.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_mizar.6cf15cbd.js": {
"file": "assets/mizar.6cf15cbd.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_mojolicious.834a438a.js": {
"file": "assets/mojolicious.834a438a.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_monkey.08e49847.js": {
"file": "assets/monkey.08e49847.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_moonscript.96b4ff81.js": {
"file": "assets/moonscript.96b4ff81.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_more-vertical.06df6aff.js": {
"file": "assets/more-vertical.06df6aff.js",
"imports": [
"index.html"
]
},
"_multi-combobox.51bcb343.js": {
"file": "assets/multi-combobox.51bcb343.js",
"imports": [
"index.html"
]
},
"_n1ql.bb73836f.js": {
"file": "assets/n1ql.bb73836f.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_nginx.83dced37.js": {
"file": "assets/nginx.83dced37.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_nim.9b03230a.js": {
"file": "assets/nim.9b03230a.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_nix.62253a60.js": {
"file": "assets/nix.62253a60.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_node-repl.e38345ae.js": {
"file": "assets/node-repl.e38345ae.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_nsis.aa91a0cd.js": {
"file": "assets/nsis.aa91a0cd.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_objectivec.636246aa.js": {
"file": "assets/objectivec.636246aa.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_ocaml.6a0416aa.js": {
"file": "assets/ocaml.6a0416aa.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_openscad.fa2654cc.js": {
"file": "assets/openscad.fa2654cc.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_ordinal.93cdc51b.js": {
"file": "assets/ordinal.93cdc51b.js",
"imports": [
"_init.a5b10ee5.js"
]
},
"_oxygene.8dc31ed9.js": {
"file": "assets/oxygene.8dc31ed9.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_pagination.524325c1.js": {
"file": "assets/pagination.524325c1.js",
"imports": [
"index.html"
]
},
"_parser3.5d447324.js": {
"file": "assets/parser3.5d447324.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_path.53f90ab3.js": {
"file": "assets/path.53f90ab3.js"
},
"_perl.9060fb02.js": {
"file": "assets/perl.9060fb02.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_pf.a4464222.js": {
"file": "assets/pf.a4464222.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_pgsql.89d8e652.js": {
"file": "assets/pgsql.89d8e652.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_php-template.02d622f8.js": {
"file": "assets/php-template.02d622f8.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_php.df7925a7.js": {
"file": "assets/php.df7925a7.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_plaintext.bb4a0b1b.js": {
"file": "assets/plaintext.bb4a0b1b.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_pony.7125a88a.js": {
"file": "assets/pony.7125a88a.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_powershell.bdb91f89.js": {
"file": "assets/powershell.bdb91f89.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_processing.160d59cc.js": {
"file": "assets/processing.160d59cc.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_profile.94a3206f.js": {
"file": "assets/profile.94a3206f.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_progress.b759e832.js": {
"file": "assets/progress.b759e832.js",
"imports": [
"index.html"
]
},
"_prolog.e57149e1.js": {
"file": "assets/prolog.e57149e1.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_properties.a1b0cc3a.js": {
"file": "assets/properties.a1b0cc3a.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_protobuf.6fd3c625.js": {
"file": "assets/protobuf.6fd3c625.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_puppet.e0eaa65e.js": {
"file": "assets/puppet.e0eaa65e.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_purebasic.d24275ac.js": {
"file": "assets/purebasic.d24275ac.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_python-repl.0f5b514e.js": {
"file": "assets/python-repl.0f5b514e.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_python.82adccf2.js": {
"file": "assets/python.82adccf2.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_q.9f7c28a1.js": {
"file": "assets/q.9f7c28a1.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_qml.116fd185.js": {
"file": "assets/qml.116fd185.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_r.42b21dcc.js": {
"file": "assets/r.42b21dcc.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_radio-group.9fad45cf.js": {
"file": "assets/radio-group.9fad45cf.js",
"imports": [
"index.html"
]
},
"_reasonml.6e89e640.js": {
"file": "assets/reasonml.6e89e640.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_request.9ae92294.js": {
"file": "assets/request.9ae92294.js",
"imports": [
"index.html"
]
},
"_rib.44b3e5d6.js": {
"file": "assets/rib.44b3e5d6.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_roboconf.e09e943c.js": {
"file": "assets/roboconf.e09e943c.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_routeros.1e8f226f.js": {
"file": "assets/routeros.1e8f226f.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_rsl.1896018e.js": {
"file": "assets/rsl.1896018e.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_ruby.f9cd1dd5.js": {
"file": "assets/ruby.f9cd1dd5.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_ruleslanguage.efb7e96b.js": {
"file": "assets/ruleslanguage.efb7e96b.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_rust.eaffbb35.js": {
"file": "assets/rust.eaffbb35.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_sas.791dadca.js": {
"file": "assets/sas.791dadca.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_save.cc03e881.js": {
"file": "assets/save.cc03e881.js",
"imports": [
"index.html"
]
},
"_scala.0ec867bf.js": {
"file": "assets/scala.0ec867bf.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_scan-barcode.437edb2c.js": {
"file": "assets/scan-barcode.437edb2c.js",
"imports": [
"index.html"
]
},
"_scheme.1acc1dfa.js": {
"file": "assets/scheme.1acc1dfa.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_scilab.25832050.js": {
"file": "assets/scilab.25832050.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_scss.e7f9a072.js": {
"file": "assets/scss.e7f9a072.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_settings-2.9ad8babc.js": {
"file": "assets/settings-2.9ad8babc.js",
"imports": [
"index.html"
]
},
"_shell.1c309f31.js": {
"file": "assets/shell.1c309f31.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_smali.fddc422d.js": {
"file": "assets/smali.fddc422d.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_smalltalk.9a1542dc.js": {
"file": "assets/smalltalk.9a1542dc.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_sml.871d736c.js": {
"file": "assets/sml.871d736c.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_sqf.d9ed268b.js": {
"file": "assets/sqf.d9ed268b.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_sql.c3b24f73.js": {
"file": "assets/sql.c3b24f73.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_sql_more.4bd7d0c7.js": {
"file": "assets/sql_more.4bd7d0c7.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_stan.7b1be381.js": {
"file": "assets/stan.7b1be381.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_stata.0a068841.js": {
"file": "assets/stata.0a068841.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_step21.4392b842.js": {
"file": "assets/step21.4392b842.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_styles-3ed67cfa.d41aab61.js": {
"file": "assets/styles-3ed67cfa.d41aab61.js",
"imports": [
"_graph.0ef9c3b7.js",
"index.html",
"_index-fc10efb0.021331b3.js",
"_channel.0e442477.js"
]
},
"_styles-991ebdfc.bd043552.js": {
"file": "assets/styles-991ebdfc.bd043552.js",
"imports": [
"index.html"
]
},
"_styles-d20c7d72.76540848.js": {
"file": "assets/styles-d20c7d72.76540848.js",
"imports": [
"index.html"
]
},
"_stylus.7b7519f0.js": {
"file": "assets/stylus.7b7519f0.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_subunit.fede25b7.js": {
"file": "assets/subunit.fede25b7.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_svgDrawCommon-5ccd53ef.362ba997.js": {
"file": "assets/svgDrawCommon-5ccd53ef.362ba997.js",
"imports": [
"index.html"
]
},
"_swift.7b385dfc.js": {
"file": "assets/swift.7b385dfc.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_system.4d03cc26.js": {
"file": "assets/system.4d03cc26.js",
"imports": [
"index.html"
]
},
"_table.b7adcb2f.js": {
"file": "assets/table.b7adcb2f.js",
"imports": [
"index.html",
"_filter.0ca1fe92.js"
]
},
"_tabs.7d7cdf4d.js": {
"file": "assets/tabs.7d7cdf4d.js",
"imports": [
"index.html"
]
},
"_taggerscript.a5a3c164.js": {
"file": "assets/taggerscript.a5a3c164.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_tap.ec001561.js": {
"file": "assets/tap.ec001561.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_tcl.5faa7b55.js": {
"file": "assets/tcl.5faa7b55.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_thrift.501d09ba.js": {
"file": "assets/thrift.501d09ba.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_time.f5c88166.js": {
"file": "assets/time.f5c88166.js",
"imports": [
"_linear.1519afab.js",
"_init.a5b10ee5.js"
]
},
"_tiny-invariant.dd7d57d2.js": {
"file": "assets/tiny-invariant.dd7d57d2.js"
},
"_tp.1545d2cf.js": {
"file": "assets/tp.1545d2cf.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_twig.0d7d2341.js": {
"file": "assets/twig.0d7d2341.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_typescript.d128319f.js": {
"file": "assets/typescript.d128319f.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_upload-cloud.2f8ed3bd.js": {
"file": "assets/upload-cloud.2f8ed3bd.js",
"imports": [
"index.html"
]
},
"_vala.c59c1f8e.js": {
"file": "assets/vala.c59c1f8e.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_vbnet.6c9093e0.js": {
"file": "assets/vbnet.6c9093e0.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_vbscript-html.d290f5d5.js": {
"file": "assets/vbscript-html.d290f5d5.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_vbscript.3b9bcd96.js": {
"file": "assets/vbscript.3b9bcd96.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_verilog.9242d940.js": {
"file": "assets/verilog.9242d940.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_vhdl.aed608da.js": {
"file": "assets/vhdl.aed608da.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_vim.6dc8565f.js": {
"file": "assets/vim.6dc8565f.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_x86asm.ef055c19.js": {
"file": "assets/x86asm.ef055c19.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_xl.dce5d508.js": {
"file": "assets/xl.dce5d508.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_xml.1a48b5b5.js": {
"file": "assets/xml.1a48b5b5.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_xquery.5286f839.js": {
"file": "assets/xquery.5286f839.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_yaml.bff970e0.js": {
"file": "assets/yaml.bff970e0.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"_zephir.5f89667a.js": {
"file": "assets/zephir.5f89667a.js",
"imports": [
"index.html"
],
"isDynamicEntry": true
},
"index.css": {
"file": "assets/index-21f57514.css",
"src": "index.css"
},
"index.html": {
"css": [
"assets/index-21f57514.css"
],
"dynamicImports": [
"_1c.7075dba8.js",
"_abnf.9bf3a7c1.js",
"_accesslog.68f2b66c.js",
"_actionscript.67a1e568.js",
"_ada.c0e23f60.js",
"_angelscript.410ffc68.js",
"_apache.006764de.js",
"_applescript.39299b7d.js",
"_arcade.f590a3cc.js",
"_arduino.c4c70db8.js",
"_armasm.50cdbe9a.js",
"_asciidoc.e1e2cbee.js",
"_aspectj.a696b879.js",
"_autohotkey.c54238fc.js",
"_autoit.25b13806.js",
"_avrasm.a69814df.js",
"_awk.9f65b5a4.js",
"_axapta.fa0aef8d.js",
"_bash.a8e19ccc.js",
"_basic.6acc6ac9.js",
"_bnf.bd11f475.js",
"_brainfuck.db01d938.js",
"_c-like.af826c24.js",
"_c.cc4c2414.js",
"_cal.b721a23c.js",
"_capnproto.7f37471d.js",
"_ceylon.f41d8cb9.js",
"_clean.2230529c.js",
"_clojure-repl.24369284.js",
"_clojure.98540e11.js",
"_cmake.687d96e9.js",
"_coffeescript.09ac2f3f.js",
"_coq.18e8d32c.js",
"_cos.cfa5f5a1.js",
"_cpp.a6b75908.js",
"_crmsh.929b5790.js",
"_crystal.520aae80.js",
"_csharp.676d187e.js",
"_csp.27c6fdf5.js",
"_css.aed2fec3.js",
"_d.deee4072.js",
"_dart.86736099.js",
"_delphi.b2e83fb0.js",
"_diff.52ac5061.js",
"_django.acf6f5d0.js",
"_dns.ae462891.js",
"_dockerfile.a461ef01.js",
"_dos.66ec8774.js",
"_dsconfig.948d33c9.js",
"_dts.ace8285a.js",
"_dust.61c6165d.js",
"_ebnf.9f47f2bf.js",
"_elixir.87b77a7b.js",
"_elm.62fc5561.js",
"_erb.55b07d6c.js",
"_erlang-repl.323f7160.js",
"_erlang.c7e57743.js",
"_excel.4522222f.js",
"_fix.51ea128a.js",
"_flix.c3fcc323.js",
"_fortran.5261c416.js",
"_fsharp.11981ee5.js",
"_gams.49652455.js",
"_gauss.012009e3.js",
"_gcode.0c400ff3.js",
"_gherkin.6750692c.js",
"_glsl.3aa8f677.js",
"_gml.8a3d692a.js",
"_go.9e6e090c.js",
"_golo.0c3cd41a.js",
"_gradle.9b2be066.js",
"_groovy.e5a1e4e3.js",
"_haml.0925c5ed.js",
"_handlebars.a705eec6.js",
"_haskell.c1c10057.js",
"_haxe.bb1f4576.js",
"_hsp.acbd24bb.js",
"_htmlbars.ae56fbaf.js",
"_http.cadf2244.js",
"_hy.94f04bbe.js",
"_inform7.573ba170.js",
"_ini.0ce80bb9.js",
"_irpf90.a8791531.js",
"_isbl.17bd57b9.js",
"_java.de21cb0c.js",
"_javascript.4b296926.js",
"_jboss-cli.f0eb5633.js",
"_json.6e716f3a.js",
"_julia-repl.6f5d2fd3.js",
"_julia.41a3881b.js",
"_kotlin.c59af0ba.js",
"_lasso.8a28f498.js",
"_latex.058112d0.js",
"_ldif.e8f34ce7.js",
"_leaf.9ed3302f.js",
"_less.1e2f29fc.js",
"_lisp.ab804084.js",
"_livecodeserver.ada7c599.js",
"_livescript.09fbd630.js",
"_llvm.c75c9c59.js",
"_lsl.21a37c7a.js",
"_lua.c0a16d09.js",
"_makefile.bb0fcfbd.js",
"_markdown.26dd2c01.js",
"_mathematica.2dc23a04.js",
"_matlab.e39e79ce.js",
"_maxima.b42deb33.js",
"_mel.30384c03.js",
"_mercury.4f44fcae.js",
"_mipsasm.6d204ed1.js",
"_mizar.6cf15cbd.js",
"_mojolicious.834a438a.js",
"_monkey.08e49847.js",
"_moonscript.96b4ff81.js",
"_n1ql.bb73836f.js",
"_nginx.83dced37.js",
"_nim.9b03230a.js",
"_nix.62253a60.js",
"_node-repl.e38345ae.js",
"_nsis.aa91a0cd.js",
"_objectivec.636246aa.js",
"_ocaml.6a0416aa.js",
"_openscad.fa2654cc.js",
"_oxygene.8dc31ed9.js",
"_parser3.5d447324.js",
"_perl.9060fb02.js",
"_pf.a4464222.js",
"_pgsql.89d8e652.js",
"_php-template.02d622f8.js",
"_php.df7925a7.js",
"_plaintext.bb4a0b1b.js",
"_pony.7125a88a.js",
"_powershell.bdb91f89.js",
"_processing.160d59cc.js",
"_profile.94a3206f.js",
"_prolog.e57149e1.js",
"_properties.a1b0cc3a.js",
"_protobuf.6fd3c625.js",
"_puppet.e0eaa65e.js",
"_purebasic.d24275ac.js",
"_python-repl.0f5b514e.js",
"_python.82adccf2.js",
"_q.9f7c28a1.js",
"_qml.116fd185.js",
"_r.42b21dcc.js",
"_reasonml.6e89e640.js",
"_rib.44b3e5d6.js",
"_roboconf.e09e943c.js",
"_routeros.1e8f226f.js",
"_rsl.1896018e.js",
"_ruby.f9cd1dd5.js",
"_ruleslanguage.efb7e96b.js",
"_rust.eaffbb35.js",
"_sas.791dadca.js",
"_scala.0ec867bf.js",
"_scheme.1acc1dfa.js",
"_scilab.25832050.js",
"_scss.e7f9a072.js",
"_shell.1c309f31.js",
"_smali.fddc422d.js",
"_smalltalk.9a1542dc.js",
"_sml.871d736c.js",
"_sqf.d9ed268b.js",
"_sql.c3b24f73.js",
"_sql_more.4bd7d0c7.js",
"_stan.7b1be381.js",
"_stata.0a068841.js",
"_step21.4392b842.js",
"_stylus.7b7519f0.js",
"_subunit.fede25b7.js",
"_swift.7b385dfc.js",
"_taggerscript.a5a3c164.js",
"_tap.ec001561.js",
"_tcl.5faa7b55.js",
"_thrift.501d09ba.js",
"_tp.1545d2cf.js",
"_twig.0d7d2341.js",
"_typescript.d128319f.js",
"_vala.c59c1f8e.js",
"_vbnet.6c9093e0.js",
"_vbscript-html.d290f5d5.js",
"_vbscript.3b9bcd96.js",
"_verilog.9242d940.js",
"_vhdl.aed608da.js",
"_vim.6dc8565f.js",
"_x86asm.ef055c19.js",
"_xl.dce5d508.js",
"_xml.1a48b5b5.js",
"_xquery.5286f839.js",
"_yaml.bff970e0.js",
"_zephir.5f89667a.js",
"_core.b8b80318.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/c4Diagram-b2a90758.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowDiagram-5540d9b9.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowDiagram-v2-3b53844e.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/erDiagram-47591fe2.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/gitGraphDiagram-96e6b4ee.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/ganttDiagram-9a3bba1f.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/infoDiagram-bcd20f53.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/pieDiagram-79897490.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/quadrantDiagram-62f64e94.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/xychartDiagram-ab372869.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/requirementDiagram-05bf5f74.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/sequenceDiagram-acc0e65c.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/classDiagram-30eddba6.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/classDiagram-v2-f2df5561.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/stateDiagram-0ff1cf1a.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/stateDiagram-v2-9a9d610d.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/journeyDiagram-4fe6b3dc.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowchart-elk-definition-5fe447d6.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/timeline-definition-fea2a41d.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/mindmap-definition-f354de21.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/sankeyDiagram-97764748.js",
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/blockDiagram-91b80b7a.js",
"src/routes/Model.tsx",
"src/routes/Wallet.tsx",
"src/routes/Record.tsx",
"src/routes/Preset.tsx",
"src/routes/Account.tsx",
"src/routes/Generation.tsx",
"src/routes/Sharing.tsx",
"src/routes/Article.tsx",
"src/routes/Admin.tsx",
"src/routes/admin/DashBoard.tsx",
"src/routes/admin/Market.tsx",
"src/routes/admin/Channel.tsx",
"src/routes/admin/System.tsx",
"src/routes/admin/Charge.tsx",
"src/routes/admin/Users.tsx",
"src/routes/admin/Broadcast.tsx",
"src/routes/admin/Subscription.tsx",
"src/routes/admin/Record.tsx",
"src/routes/admin/Payment.tsx",
"src/routes/admin/Logger.tsx",
"src/routes/EpayNotify.tsx"
],
"file": "assets/index.8d7e6a5a.js",
"isEntry": true,
"src": "index.html"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/blockDiagram-91b80b7a.js": {
"file": "assets/blockDiagram-91b80b7a.047c225c.js",
"imports": [
"index.html",
"_clone.2acf797d.js",
"_edges-d32062c0.bb379725.js",
"_graph.0ef9c3b7.js",
"_ordinal.93cdc51b.js",
"_Tableau10.1b767f5e.js",
"_channel.0e442477.js",
"_createText-6b48ae7d.01af0827.js",
"_line.c9b6b09c.js",
"_array.9f3ba611.js",
"_path.53f90ab3.js",
"_init.a5b10ee5.js"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/blockDiagram-91b80b7a.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/c4Diagram-b2a90758.js": {
"file": "assets/c4Diagram-b2a90758.e9bd027f.js",
"imports": [
"index.html",
"_svgDrawCommon-5ccd53ef.362ba997.js"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/c4Diagram-b2a90758.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/classDiagram-30eddba6.js": {
"file": "assets/classDiagram-30eddba6.e8f5c5df.js",
"imports": [
"_styles-991ebdfc.bd043552.js",
"index.html",
"_graph.0ef9c3b7.js",
"_layout.991ed0c6.js",
"_line.c9b6b09c.js",
"_array.9f3ba611.js",
"_path.53f90ab3.js"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/classDiagram-30eddba6.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/classDiagram-v2-f2df5561.js": {
"file": "assets/classDiagram-v2-f2df5561.ba89e794.js",
"imports": [
"_styles-991ebdfc.bd043552.js",
"index.html",
"_graph.0ef9c3b7.js",
"_index-fc10efb0.021331b3.js",
"_layout.991ed0c6.js",
"_clone.2acf797d.js",
"_edges-d32062c0.bb379725.js",
"_createText-6b48ae7d.01af0827.js",
"_line.c9b6b09c.js",
"_array.9f3ba611.js",
"_path.53f90ab3.js"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/classDiagram-v2-f2df5561.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/erDiagram-47591fe2.js": {
"file": "assets/erDiagram-47591fe2.299b5ec2.js",
"imports": [
"index.html",
"_graph.0ef9c3b7.js",
"_layout.991ed0c6.js",
"_line.c9b6b09c.js",
"_array.9f3ba611.js",
"_path.53f90ab3.js"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/erDiagram-47591fe2.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowDiagram-5540d9b9.js": {
"file": "assets/flowDiagram-5540d9b9.675a4ea5.js",
"imports": [
"_flowDb-4b19a42f.0b3ed11f.js",
"_graph.0ef9c3b7.js",
"index.html",
"_layout.991ed0c6.js",
"_styles-3ed67cfa.d41aab61.js",
"_line.c9b6b09c.js",
"_index-fc10efb0.021331b3.js",
"_clone.2acf797d.js",
"_edges-d32062c0.bb379725.js",
"_createText-6b48ae7d.01af0827.js",
"_channel.0e442477.js",
"_array.9f3ba611.js",
"_path.53f90ab3.js"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowDiagram-5540d9b9.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowDiagram-v2-3b53844e.js": {
"file": "assets/flowDiagram-v2-3b53844e.cc42dd61.js",
"imports": [
"_flowDb-4b19a42f.0b3ed11f.js",
"_styles-3ed67cfa.d41aab61.js",
"index.html",
"_graph.0ef9c3b7.js",
"_layout.991ed0c6.js",
"_index-fc10efb0.021331b3.js",
"_clone.2acf797d.js",
"_edges-d32062c0.bb379725.js",
"_createText-6b48ae7d.01af0827.js",
"_line.c9b6b09c.js",
"_array.9f3ba611.js",
"_path.53f90ab3.js",
"_channel.0e442477.js"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowDiagram-v2-3b53844e.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowchart-elk-definition-5fe447d6.js": {
"file": "assets/flowchart-elk-definition-5fe447d6.f3337586.js",
"imports": [
"_flowDb-4b19a42f.0b3ed11f.js",
"index.html",
"_edges-d32062c0.bb379725.js",
"_line.c9b6b09c.js",
"_createText-6b48ae7d.01af0827.js",
"_array.9f3ba611.js",
"_path.53f90ab3.js"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/flowchart-elk-definition-5fe447d6.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/ganttDiagram-9a3bba1f.js": {
"file": "assets/ganttDiagram-9a3bba1f.f22e0aee.js",
"imports": [
"index.html",
"_time.f5c88166.js",
"_linear.1519afab.js",
"_init.a5b10ee5.js"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/ganttDiagram-9a3bba1f.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/gitGraphDiagram-96e6b4ee.js": {
"file": "assets/gitGraphDiagram-96e6b4ee.d53591a6.js",
"imports": [
"index.html"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/gitGraphDiagram-96e6b4ee.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/infoDiagram-bcd20f53.js": {
"file": "assets/infoDiagram-bcd20f53.23cab256.js",
"imports": [
"index.html"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/infoDiagram-bcd20f53.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/journeyDiagram-4fe6b3dc.js": {
"file": "assets/journeyDiagram-4fe6b3dc.6ea4bcd5.js",
"imports": [
"index.html",
"_svgDrawCommon-5ccd53ef.362ba997.js",
"_arc.a247e62c.js",
"_path.53f90ab3.js"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/journeyDiagram-4fe6b3dc.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/mindmap-definition-f354de21.js": {
"file": "assets/mindmap-definition-f354de21.8698e7a8.js",
"imports": [
"index.html",
"__isIndex.00b9330c.js",
"_createText-6b48ae7d.01af0827.js"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/mindmap-definition-f354de21.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/pieDiagram-79897490.js": {
"file": "assets/pieDiagram-79897490.8acd8555.js",
"imports": [
"index.html",
"_arc.a247e62c.js",
"_ordinal.93cdc51b.js",
"_array.9f3ba611.js",
"_path.53f90ab3.js",
"_init.a5b10ee5.js"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/pieDiagram-79897490.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/quadrantDiagram-62f64e94.js": {
"file": "assets/quadrantDiagram-62f64e94.479e2729.js",
"imports": [
"index.html",
"_linear.1519afab.js",
"_init.a5b10ee5.js"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/quadrantDiagram-62f64e94.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/requirementDiagram-05bf5f74.js": {
"file": "assets/requirementDiagram-05bf5f74.383dd160.js",
"imports": [
"index.html",
"_graph.0ef9c3b7.js",
"_layout.991ed0c6.js",
"_line.c9b6b09c.js",
"_array.9f3ba611.js",
"_path.53f90ab3.js"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/requirementDiagram-05bf5f74.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/sankeyDiagram-97764748.js": {
"file": "assets/sankeyDiagram-97764748.3efa2305.js",
"imports": [
"index.html",
"_ordinal.93cdc51b.js",
"_Tableau10.1b767f5e.js",
"_init.a5b10ee5.js"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/sankeyDiagram-97764748.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/sequenceDiagram-acc0e65c.js": {
"file": "assets/sequenceDiagram-acc0e65c.1f830b15.js",
"imports": [
"index.html",
"_svgDrawCommon-5ccd53ef.362ba997.js"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/sequenceDiagram-acc0e65c.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/stateDiagram-0ff1cf1a.js": {
"file": "assets/stateDiagram-0ff1cf1a.af40e4d4.js",
"imports": [
"_styles-d20c7d72.76540848.js",
"index.html",
"_graph.0ef9c3b7.js",
"_layout.991ed0c6.js",
"_line.c9b6b09c.js",
"_array.9f3ba611.js",
"_path.53f90ab3.js"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/stateDiagram-0ff1cf1a.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/stateDiagram-v2-9a9d610d.js": {
"file": "assets/stateDiagram-v2-9a9d610d.5e37d388.js",
"imports": [
"_styles-d20c7d72.76540848.js",
"_graph.0ef9c3b7.js",
"index.html",
"_index-fc10efb0.021331b3.js",
"_layout.991ed0c6.js",
"_clone.2acf797d.js",
"_edges-d32062c0.bb379725.js",
"_createText-6b48ae7d.01af0827.js",
"_line.c9b6b09c.js",
"_array.9f3ba611.js",
"_path.53f90ab3.js"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/stateDiagram-v2-9a9d610d.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/timeline-definition-fea2a41d.js": {
"file": "assets/timeline-definition-fea2a41d.36bf6e4d.js",
"imports": [
"index.html",
"_arc.a247e62c.js",
"_path.53f90ab3.js"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/timeline-definition-fea2a41d.js"
},
"node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/xychartDiagram-ab372869.js": {
"file": "assets/xychartDiagram-ab372869.3444a59b.js",
"imports": [
"index.html",
"_createText-6b48ae7d.01af0827.js",
"_band.6ffec690.js",
"_linear.1519afab.js",
"_line.c9b6b09c.js",
"_init.a5b10ee5.js",
"_ordinal.93cdc51b.js",
"_array.9f3ba611.js",
"_path.53f90ab3.js"
],
"isDynamicEntry": true,
"src": "node_modules/.pnpm/mermaid@10.9.0/node_modules/mermaid/dist/xychartDiagram-ab372869.js"
},
"src/routes/Account.css": {
"file": "assets/Account-b93fa699.css",
"src": "src/routes/Account.css"
},
"src/routes/Account.tsx": {
"css": [
"assets/Account-b93fa699.css"
],
"file": "assets/Account.9f79e18e.js",
"imports": [
"index.html",
"_calendar-clock.1432b482.js",
"_gift.b3e4cc82.js",
"_clock.36b36b21.js"
],
"isDynamicEntry": true,
"src": "src/routes/Account.tsx"
},
"src/routes/Admin.css": {
"file": "assets/Admin-502861ce.css",
"src": "src/routes/Admin.css"
},
"src/routes/Admin.tsx": {
"css": [
"assets/Admin-502861ce.css"
],
"file": "assets/Admin.f2a9d119.js",
"imports": [
"index.html",
"_history.44d149e5.js"
],
"isDynamicEntry": true,
"src": "src/routes/Admin.tsx"
},
"src/routes/Article.css": {
"file": "assets/Article-82d9a1fc.css",
"src": "src/routes/Article.css"
},
"src/routes/Article.tsx": {
"css": [
"assets/Article-82d9a1fc.css"
],
"file": "assets/Article.58956150.js",
"imports": [
"index.html",
"_progress.b759e832.js"
],
"isDynamicEntry": true,
"src": "src/routes/Article.tsx"
},
"src/routes/EpayNotify.css": {
"file": "assets/EpayNotify-b82ac077.css",
"src": "src/routes/EpayNotify.css"
},
"src/routes/EpayNotify.tsx": {
"css": [
"assets/EpayNotify-b82ac077.css"
],
"file": "assets/EpayNotify.197de3d5.js",
"imports": [
"index.html",
"_request.9ae92294.js",
"_box.6dd688fe.js",
"_scan-barcode.437edb2c.js"
],
"isDynamicEntry": true,
"src": "src/routes/EpayNotify.tsx"
},
"src/routes/Generation.css": {
"file": "assets/Generation-7b5b93aa.css",
"src": "src/routes/Generation.css"
},
"src/routes/Generation.tsx": {
"css": [
"assets/Generation-7b5b93aa.css"
],
"file": "assets/Generation.e6964a34.js",
"imports": [
"index.html"
],
"isDynamicEntry": true,
"src": "src/routes/Generation.tsx"
},
"src/routes/Model.tsx": {
"file": "assets/Model.beec7117.js",
"imports": [
"index.html",
"_market.93d110b3.js",
"_arrow-right-left.f4c82cc0.js",
"_box.6dd688fe.js",
"_upload-cloud.2f8ed3bd.js",
"_download-cloud.04333a4f.js"
],
"isDynamicEntry": true,
"src": "src/routes/Model.tsx"
},
"src/routes/Preset.css": {
"file": "assets/Preset-8710ef85.css",
"src": "src/routes/Preset.css"
},
"src/routes/Preset.tsx": {
"css": [
"assets/Preset-8710ef85.css"
],
"file": "assets/Preset.95a9b1e0.js",
"imports": [
"index.html",
"_arrow-right-left.f4c82cc0.js"
],
"isDynamicEntry": true,
"src": "src/routes/Preset.tsx"
},
"src/routes/Record.css": {
"file": "assets/Record-dd76ad5a.css",
"src": "src/routes/Record.css"
},
"src/routes/Record.tsx": {
"css": [
"assets/Record-dd76ad5a.css"
],
"file": "assets/Record.7203fb33.js",
"imports": [
"index.html",
"_table.b7adcb2f.js",
"_Tracker.20c1771e.js",
"_pagination.524325c1.js",
"_calendar.2f96549a.js",
"_upload-cloud.2f8ed3bd.js",
"_clock.36b36b21.js",
"_activity.612642ea.js",
"_asterisk-square.2615ee58.js",
"_history.44d149e5.js",
"_filter.0ca1fe92.js",
"__isIndex.00b9330c.js",
"_tiny-invariant.dd7d57d2.js",
"_path.53f90ab3.js",
"_linear.1519afab.js",
"_init.a5b10ee5.js",
"_time.f5c88166.js",
"_band.6ffec690.js",
"_ordinal.93cdc51b.js",
"_array.9f3ba611.js",
"_line.c9b6b09c.js"
],
"isDynamicEntry": true,
"src": "src/routes/Record.tsx"
},
"src/routes/Sharing.css": {
"file": "assets/Sharing-07989aa3.css",
"src": "src/routes/Sharing.css"
},
"src/routes/Sharing.tsx": {
"css": [
"assets/Sharing-07989aa3.css"
],
"file": "assets/Sharing.b857888c.js",
"imports": [
"index.html",
"_clock.36b36b21.js"
],
"isDynamicEntry": true,
"src": "src/routes/Sharing.tsx"
},
"src/routes/Wallet.css": {
"file": "assets/Wallet-adc144da.css",
"src": "src/routes/Wallet.css"
},
"src/routes/Wallet.tsx": {
"css": [
"assets/Wallet-adc144da.css"
],
"file": "assets/Wallet.23e502a3.js",
"imports": [
"index.html",
"_request.9ae92294.js",
"_icons.7e9c9414.js",
"_progress.b759e832.js",
"_tabs.7d7cdf4d.js",
"_gift.b3e4cc82.js",
"_calendar.2f96549a.js",
"_calendar-clock.1432b482.js",
"_box.6dd688fe.js"
],
"isDynamicEntry": true,
"src": "src/routes/Wallet.tsx"
},
"src/routes/admin/Broadcast.tsx": {
"file": "assets/Broadcast.10adc6a8.js",
"imports": [
"index.html",
"_table.b7adcb2f.js",
"_more-vertical.06df6aff.js",
"_filter.0ca1fe92.js"
],
"isDynamicEntry": true,
"src": "src/routes/admin/Broadcast.tsx"
},
"src/routes/admin/Channel.tsx": {
"file": "assets/Channel.22f78415.js",
"imports": [
"index.html",
"_table.b7adcb2f.js",
"_OperationAction.5b1f951a.js",
"_hook.09e5815b.js",
"_activity.612642ea.js",
"_settings-2.9ad8babc.js",
"_asterisk-square.2615ee58.js",
"_Paragraph.4ca577cf.js",
"_multi-combobox.51bcb343.js",
"_kanban-square-dashed.c88dceb2.js",
"_book-dashed.798b0c5c.js",
"_filter.0ca1fe92.js"
],
"isDynamicEntry": true,
"src": "src/routes/admin/Channel.tsx"
},
"src/routes/admin/Charge.tsx": {
"file": "assets/Charge.9dc9e031.js",
"imports": [
"index.html",
"_radio-group.9fad45cf.js",
"_table.b7adcb2f.js",
"_OperationAction.5b1f951a.js",
"_hook.09e5815b.js",
"_kanban-square-dashed.c88dceb2.js",
"_activity.612642ea.js",
"_upload-cloud.2f8ed3bd.js",
"_download-cloud.04333a4f.js",
"_settings-2.9ad8babc.js",
"_filter.0ca1fe92.js"
],
"isDynamicEntry": true,
"src": "src/routes/admin/Charge.tsx"
},
"src/routes/admin/DashBoard.tsx": {
"file": "assets/DashBoard.a5483326.js",
"imports": [
"index.html",
"_chart.b43e29db.js",
"_Tracker.20c1771e.js",
"_multi-combobox.51bcb343.js",
"_filter.0ca1fe92.js",
"__isIndex.00b9330c.js",
"_tiny-invariant.dd7d57d2.js",
"_path.53f90ab3.js",
"_linear.1519afab.js",
"_init.a5b10ee5.js",
"_time.f5c88166.js",
"_band.6ffec690.js",
"_ordinal.93cdc51b.js",
"_array.9f3ba611.js",
"_line.c9b6b09c.js"
],
"isDynamicEntry": true,
"src": "src/routes/admin/DashBoard.tsx"
},
"src/routes/admin/Logger.tsx": {
"file": "assets/Logger.e813542f.js",
"imports": [
"index.html",
"_Paragraph.4ca577cf.js"
],
"isDynamicEntry": true,
"src": "src/routes/admin/Logger.tsx"
},
"src/routes/admin/Market.tsx": {
"file": "assets/Market.458a4f86.js",
"imports": [
"index.html",
"_market.93d110b3.js",
"_hook.09e5815b.js",
"_tiny-invariant.dd7d57d2.js",
"_system.4d03cc26.js",
"_activity.612642ea.js",
"_save.cc03e881.js"
],
"isDynamicEntry": true,
"src": "src/routes/admin/Market.tsx"
},
"src/routes/admin/Payment.tsx": {
"file": "assets/Payment.f83f77b2.js",
"imports": [
"index.html",
"_request.9ae92294.js",
"_table.b7adcb2f.js",
"_pagination.524325c1.js",
"_icons.7e9c9414.js",
"_scan-barcode.437edb2c.js",
"_more-vertical.06df6aff.js",
"_filter.0ca1fe92.js"
],
"isDynamicEntry": true,
"src": "src/routes/admin/Payment.tsx"
},
"src/routes/admin/Record.tsx": {
"file": "assets/Record.7a35bf1e.js",
"imports": [
"index.html",
"src/routes/Record.tsx",
"_table.b7adcb2f.js",
"_filter.0ca1fe92.js",
"_Tracker.20c1771e.js",
"__isIndex.00b9330c.js",
"_tiny-invariant.dd7d57d2.js",
"_path.53f90ab3.js",
"_linear.1519afab.js",
"_init.a5b10ee5.js",
"_time.f5c88166.js",
"_band.6ffec690.js",
"_ordinal.93cdc51b.js",
"_array.9f3ba611.js",
"_line.c9b6b09c.js",
"_pagination.524325c1.js",
"_calendar.2f96549a.js",
"_upload-cloud.2f8ed3bd.js",
"_clock.36b36b21.js",
"_activity.612642ea.js",
"_asterisk-square.2615ee58.js",
"_history.44d149e5.js"
],
"isDynamicEntry": true,
"src": "src/routes/admin/Record.tsx"
},
"src/routes/admin/Subscription.tsx": {
"file": "assets/Subscription.facf4385.js",
"imports": [
"index.html",
"_multi-combobox.51bcb343.js",
"_hook.09e5815b.js",
"_activity.612642ea.js",
"_save.cc03e881.js",
"_book-dashed.798b0c5c.js"
],
"isDynamicEntry": true,
"src": "src/routes/admin/Subscription.tsx"
},
"src/routes/admin/System.tsx": {
"file": "assets/System.1954c6c5.js",
"imports": [
"index.html",
"_Paragraph.4ca577cf.js",
"_system.4d03cc26.js",
"_multi-combobox.51bcb343.js",
"_hook.09e5815b.js",
"_tabs.7d7cdf4d.js",
"_icons.7e9c9414.js",
"_save.cc03e881.js",
"_settings-2.9ad8babc.js"
],
"isDynamicEntry": true,
"src": "src/routes/admin/System.tsx"
},
"src/routes/admin/Users.tsx": {
"file": "assets/Users.ee82f0be.js",
"imports": [
"index.html",
"_table.b7adcb2f.js",
"_chart.b43e29db.js",
"_pagination.524325c1.js",
"_OperationAction.5b1f951a.js",
"_radio-group.9fad45cf.js",
"_filter.0ca1fe92.js",
"_calendar-clock.1432b482.js"
],
"isDynamicEntry": true,
"src": "src/routes/admin/Users.tsx"
}
}
================================================
FILE: app/public/robots.txt
================================================
User-Agent: *
Allow: /
Disallow: /admin/
================================================
FILE: app/public/service.js
================================================
const SERVICE_NAME = "coai";
self.addEventListener('activate', function (event) {
console.debug("[service] service worker activated");
});
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open(SERVICE_NAME)
.then(function (cache) {
return cache.addAll([]);
})
);
});
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request)
.then(function (response) {
return response || fetch(event.request);
})
);
});
================================================
FILE: app/public/site.webmanifest
================================================
{
"name": "CoAI",
"short_name": "CoAI",
"icons": [
{
"src": "/service/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/service/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "/",
"theme_color": "#000",
"background_color": "#000",
"display": "standalone"
}
================================================
FILE: app/public/workbox.js
================================================
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/service.js').then(function (registration) {
console.debug(`[service] service worker registered with scope: ${registration.scope}`);
}, function (err) {
console.debug(`[service] service worker registration failed: ${err}`);
});
});
}
================================================
FILE: app/qodana.yaml
================================================
#-------------------------------------------------------------------------------#
# Qodana analysis is configured by qodana.yaml file #
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
#-------------------------------------------------------------------------------#
version: "1.0"
#Specify inspection profile for code analysis
profile:
name: qodana.starter
#Enable inspections
#include:
# - name:
#Disable inspections
#exclude:
# - name:
# paths:
# -
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
#bootstrap: sh ./prepare-qodana.sh
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
#plugins:
# - id: #(plugin id can be found at https://plugins.jetbrains.com)
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
linter: jetbrains/qodana-js:latest
================================================
FILE: app/src/App.tsx
================================================
import { Provider } from "react-redux";
import store from "./store/index.ts";
import AppProvider from "./components/app/AppProvider.tsx";
import { AppRouter } from "./router.tsx";
import { Toaster } from "@/components/ui/sonner";
import Spinner from "@/spinner.tsx";
import ReloadPrompt from "@/components/ReloadService.tsx";
function App() {
return (
);
}
export default App;
================================================
FILE: app/src/admin/api/channel.ts
================================================
import { Channel } from "@/admin/channel.ts";
import axios from "axios";
import { getErrorMessage } from "@/utils/base.ts";
import { CommonResponse } from "@/api/common.ts";
export type ChannelListResponse = CommonResponse & {
data: Channel[];
};
export type GetChannelResponse = CommonResponse & {
data?: Channel;
};
export async function listChannel(): Promise {
try {
const response = await axios.get("/admin/channel/list");
return response.data as ChannelListResponse;
} catch (e) {
return { status: false, error: getErrorMessage(e), data: [] };
}
}
export async function getChannel(id: number): Promise {
try {
const response = await axios.get(`/admin/channel/get/${id}`);
return response.data as GetChannelResponse;
} catch (e) {
return { status: false, error: getErrorMessage(e) };
}
}
export async function createChannel(channel: Channel): Promise {
try {
const response = await axios.post("/admin/channel/create", channel);
return response.data as CommonResponse;
} catch (e) {
return { status: false, error: getErrorMessage(e) };
}
}
export async function updateChannel(
id: number,
channel: Channel,
): Promise {
try {
const response = await axios.post(`/admin/channel/update/${id}`, channel);
return response.data as CommonResponse;
} catch (e) {
return { status: false, error: getErrorMessage(e) };
}
}
export async function deleteChannel(id: number): Promise {
try {
const response = await axios.get(`/admin/channel/delete/${id}`);
return response.data as CommonResponse;
} catch (e) {
return { status: false, error: getErrorMessage(e) };
}
}
export async function activateChannel(id: number): Promise {
try {
const response = await axios.get(`/admin/channel/activate/${id}`);
return response.data as CommonResponse;
} catch (e) {
return { status: false, error: getErrorMessage(e) };
}
}
export async function deactivateChannel(id: number): Promise {
try {
const response = await axios.get(`/admin/channel/deactivate/${id}`);
return response.data as CommonResponse;
} catch (e) {
return { status: false, error: getErrorMessage(e) };
}
}
================================================
FILE: app/src/admin/api/charge.ts
================================================
import { CommonResponse } from "@/api/common.ts";
import { ChargeProps } from "@/admin/charge.ts";
import { getErrorMessage } from "@/utils/base.ts";
import axios from "axios";
export type ChargeListResponse = CommonResponse & {
data: ChargeProps[];
};
export type ChargeSyncRequest = {
overwrite: boolean;
data: ChargeProps[];
};
export type ChargeFetchRequest = {
endpoint: string;
system?: string;
};
export type ChargeFetchResponse = CommonResponse & {
data: ChargeProps[];
};
export async function listCharge(): Promise {
try {
const response = await axios.get("/admin/charge/list");
return response.data as ChargeListResponse;
} catch (e) {
return { status: false, error: getErrorMessage(e), data: [] };
}
}
export async function setCharge(charge: ChargeProps): Promise {
try {
const response = await axios.post(`/admin/charge/set`, charge);
return response.data as CommonResponse;
} catch (e) {
return { status: false, error: getErrorMessage(e) };
}
}
export async function deleteCharge(id: number): Promise {
try {
const response = await axios.get(`/admin/charge/delete/${id}`);
return response.data as CommonResponse;
} catch (e) {
return { status: false, error: getErrorMessage(e) };
}
}
export async function syncCharge(
data: ChargeSyncRequest,
): Promise {
try {
const response = await axios.post(`/admin/charge/sync`, data);
return response.data as CommonResponse;
} catch (e) {
return { status: false, error: getErrorMessage(e) };
}
}
export async function fetchUpstreamCharge(
req: ChargeFetchRequest,
): Promise {
try {
const response = await axios.post(`/admin/charge/fetch`, req);
const data = response.data as ChargeFetchResponse;
return {
status: !!data.status,
error: data.error,
data: data.data || [],
};
} catch (e) {
return { status: false, error: getErrorMessage(e), data: [] };
}
}
================================================
FILE: app/src/admin/api/chart.ts
================================================
import {
BillingChartResponse,
CommonResponse,
ErrorChartResponse,
InfoResponse,
InvitationGenerateResponse,
InvitationResponse,
ModelChartResponse,
RedeemResponse,
RequestChartResponse,
UserResponse,
UserTypeChartResponse,
} from "@/admin/types.ts";
import axios from "axios";
import { getErrorMessage } from "@/utils/base.ts";
export const initialAdminInfoState: InfoResponse = {
subscription_count: 0,
billing_today: 0,
billing_month: 0,
online_chats: 0,
billing_yesterday: 0,
billing_last_month: 0,
};
export type UserFilterProps = {
plan: string;
admin: string;
ban: string;
sort: string;
};
export const initialUserFilter: UserFilterProps = {
plan: "all", // all/no/yes
admin: "all", // all/no/yes
ban: "all", // all/no/yes
sort: "id-asc",
// id-asc/id-desc
// quota-asc/quota-desc
// used-quota-asc/used-quota-desc
// plan-desc/plan-asc
};
export async function getAdminInfo(): Promise {
try {
const response = await axios.get("/admin/analytics/info");
return response.data as InfoResponse;
} catch (e) {
console.warn(e);
return {
...initialAdminInfoState,
};
}
}
export async function getModelChart(): Promise {
try {
const response = await axios.get("/admin/analytics/model");
const data = response.data as ModelChartResponse;
return {
date: data.date,
value: data.value || [],
};
} catch (e) {
console.warn(e);
return { date: [], value: [] };
}
}
export async function getRequestChart(): Promise {
try {
const response = await axios.get("/admin/analytics/request");
return response.data as RequestChartResponse;
} catch (e) {
console.warn(e);
return { date: [], value: [] };
}
}
export async function getBillingChart(): Promise {
try {
const response = await axios.get("/admin/analytics/billing");
return response.data as BillingChartResponse;
} catch (e) {
console.warn(e);
return { date: [], value: [] };
}
}
export async function getErrorChart(): Promise {
try {
const response = await axios.get("/admin/analytics/error");
return response.data as ErrorChartResponse;
} catch (e) {
console.warn(e);
return { date: [], value: [] };
}
}
export async function getUserTypeChart(): Promise {
try {
const response = await axios.get("/admin/analytics/user");
return response.data as UserTypeChartResponse;
} catch (e) {
console.warn(e);
return {
total: 0,
normal: 0,
api_paid: 0,
basic_plan: 0,
standard_plan: 0,
pro_plan: 0,
};
}
}
export async function getInvitationList(
page: number,
): Promise {
try {
const response = await axios.get(`/admin/invitation/list?page=${page}`);
return response.data as InvitationResponse;
} catch (e) {
return {
status: false,
message: getErrorMessage(e),
data: [],
total: 0,
};
}
}
export async function deleteInvitation(code: string): Promise {
try {
const response = await axios.post("/admin/invitation/delete", { code });
return response.data as CommonResponse;
} catch (e) {
return { status: false, message: getErrorMessage(e) };
}
}
export async function generateInvitation(
type: string,
quota: number,
number: number,
): Promise {
try {
const response = await axios.post("/admin/invitation/generate", {
type,
quota,
number,
});
return response.data as InvitationGenerateResponse;
} catch (e) {
return { status: false, data: [], message: getErrorMessage(e) };
}
}
export async function getRedeemList(page: number): Promise {
try {
const response = await axios.get(`/admin/redeem/list?page=${page}`);
return response.data as RedeemResponse;
} catch (e) {
console.warn(e);
return { status: false, message: getErrorMessage(e), data: [], total: 0 };
}
}
export async function deleteRedeem(code: string): Promise {
try {
const response = await axios.post("/admin/redeem/delete", { code });
return response.data as CommonResponse;
} catch (e) {
return { status: false, message: getErrorMessage(e) };
}
}
export async function generateRedeem(
quota: number,
number: number,
): Promise {
try {
const response = await axios.post("/admin/redeem/generate", {
quota,
number,
});
return response.data as InvitationGenerateResponse;
} catch (e) {
return { status: false, data: [], message: getErrorMessage(e) };
}
}
export async function getUserList(
page: number,
search: string,
params: UserFilterProps,
): Promise {
try {
const response = await axios.get(`/admin/user/list`, {
params: {
page,
search,
params,
},
});
return response.data as UserResponse;
} catch (e) {
return {
status: false,
message: getErrorMessage(e),
data: [],
total: 0,
};
}
}
export async function updatePassword(
id: number,
password: string,
): Promise {
try {
const response = await axios.post("/admin/user/password", {
id,
password,
});
return response.data as CommonResponse;
} catch (e) {
return { status: false, message: getErrorMessage(e) };
}
}
export async function updateEmail(
id: number,
email: string,
): Promise {
try {
const response = await axios.post("/admin/user/email", {
id,
email,
});
return response.data as CommonResponse;
} catch (e) {
return { status: false, message: getErrorMessage(e) };
}
}
export async function quotaOperation(
id: number,
quota: number,
override?: boolean,
): Promise {
try {
const response = await axios.post("/admin/user/quota", {
id,
quota,
override: override ?? false,
});
return response.data as CommonResponse;
} catch (e) {
return { status: false, message: getErrorMessage(e) };
}
}
export async function subscriptionOperation(
id: number,
expired: string,
): Promise {
try {
const response = await axios.post("/admin/user/subscription", {
id,
expired,
});
return response.data as CommonResponse;
} catch (e) {
return { status: false, message: getErrorMessage(e) };
}
}
export async function banUserOperation(
id: number,
ban: boolean,
): Promise {
try {
const response = await axios.post("/admin/user/ban", {
id,
ban,
});
return response.data as CommonResponse;
} catch (e) {
return { status: false, message: getErrorMessage(e) };
}
}
export async function setAdminOperation(
id: number,
admin: boolean,
): Promise {
try {
const response = await axios.post("/admin/user/admin", {
id,
admin,
});
return response.data as CommonResponse;
} catch (e) {
return { status: false, message: getErrorMessage(e) };
}
}
export async function subscriptionLevelOperation(
id: number,
level: number,
): Promise {
try {
const response = await axios.post("/admin/user/level", { id, level });
return response.data as CommonResponse;
} catch (e) {
return { status: false, message: getErrorMessage(e) };
}
}
export async function releaseUsageOperation(
id: number,
): Promise {
try {
const response = await axios.post("/admin/user/release", { id });
return response.data as CommonResponse;
} catch (e) {
return { status: false, message: getErrorMessage(e) };
}
}
================================================
FILE: app/src/admin/api/info.ts
================================================
import axios from "axios";
import {
setAppLogo,
setAppName,
setBlobEndpoint,
setBuyLink,
setDocsUrl,
} from "@/conf/env.ts";
import { infoEvent } from "@/events/info.ts";
import { initGoogleAnalytics } from "@/utils/analytics.ts";
import { BroadcastEvent, getBroadcast } from "@/api/broadcast";
export type SiteInfo = {
title: string;
logo: string;
docs: string;
file: string;
backend?: string;
currency: string;
announcement: string;
buy_link: string;
mail: boolean;
contact: string;
footer: string;
auth_footer: boolean;
hide_key_docs?: boolean;
article: string[];
generation: string[];
relay_plan: boolean;
payment: string[];
payment_aggregation: boolean;
ga_tracking_id?: string;
broadcast?: BroadcastEvent;
};
export async function getSiteInfo(): Promise {
try {
const response = await axios.get("/info");
return response.data as SiteInfo;
} catch (e) {
console.warn(e);
return {
title: "",
logo: "",
docs: "",
file: "",
backend: undefined,
currency: "cny",
announcement: "",
buy_link: "",
contact: "",
footer: "",
auth_footer: false,
hide_key_docs: false,
mail: false,
article: [],
generation: [],
relay_plan: false,
payment: [],
payment_aggregation: false,
broadcast: {
message: "",
firstReceived: false,
},
};
}
}
export function syncSiteInfo() {
setTimeout(async () => {
const info = await getSiteInfo();
info.broadcast = await getBroadcast();
setAppName(info.title);
setAppLogo(info.logo);
setDocsUrl(info.docs);
setBlobEndpoint(info.file);
setBuyLink(info.buy_link);
initGoogleAnalytics(info.ga_tracking_id);
infoEvent.emit(info);
}, 25);
}
================================================
FILE: app/src/admin/api/logger.ts
================================================
import axios from "axios";
import { CommonResponse } from "@/api/common.ts";
import { getErrorMessage } from "@/utils/base.ts";
export type Logger = {
path: string;
size: number;
};
export async function listLoggers(): Promise {
try {
const response = await axios.get("/admin/logger/list");
return (response.data || []) as Logger[];
} catch (e) {
console.warn(e);
return [];
}
}
export async function getLoggerConsole(n?: number): Promise {
try {
const response = await axios.get(`/admin/logger/console?n=${n ?? 100}`);
return response.data.content as string;
} catch (e) {
console.warn(e);
return `failed to get info from server: ${getErrorMessage(e)}`;
}
}
export async function downloadLogger(path: string): Promise {
try {
const response = await axios.get("/admin/logger/download", {
responseType: "blob",
params: { path },
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", path);
document.body.appendChild(link);
link.click();
} catch (e) {
console.warn(e);
}
}
export async function deleteLogger(path: string): Promise {
try {
const response = await axios.post(`/admin/logger/delete?path=${path}`);
return response.data as CommonResponse;
} catch (e) {
console.warn(e);
return { status: false, error: getErrorMessage(e) };
}
}
================================================
FILE: app/src/admin/api/market.ts
================================================
import { Model } from "@/api/types.tsx";
import { CommonResponse } from "@/api/common.ts";
import axios from "axios";
import { getErrorMessage } from "@/utils/base.ts";
export async function updateMarket(data: Model[]): Promise {
try {
const resp = await axios.post("/admin/market/update", data);
return resp.data as CommonResponse;
} catch (e) {
console.warn(e);
return { status: false, error: getErrorMessage(e) };
}
}
================================================
FILE: app/src/admin/api/plan.ts
================================================
import { Plan } from "@/api/types.tsx";
import axios from "axios";
import { CommonResponse } from "@/api/common.ts";
import { getErrorMessage } from "@/utils/base.ts";
import { getApiPlans } from "@/api/v1.ts";
export type PlanConfig = {
enabled: boolean;
plans: Plan[];
};
export async function getPlanConfig(): Promise {
try {
const response = await axios.get("/admin/plan/view");
const conf = response.data as PlanConfig;
conf.plans = (conf.plans || []).filter((item) => item.level > 0);
if (conf.plans.length === 0)
conf.plans = [1, 2, 3].map(
(level) => ({ level, price: 0, items: [] }) as Plan,
);
return conf;
} catch (e) {
console.warn(e);
return { enabled: false, plans: [] };
}
}
export async function getExternalPlanConfig(
endpoint: string,
): Promise {
const response = await getApiPlans({ endpoint });
return { enabled: response.length > 0, plans: response };
}
export async function setPlanConfig(
config: PlanConfig,
): Promise {
try {
const response = await axios.post(`/admin/plan/update`, config);
return response.data as CommonResponse;
} catch (e) {
return { status: false, error: getErrorMessage(e) };
}
}
================================================
FILE: app/src/admin/api/system.ts
================================================
import { CommonResponse } from "@/api/common.ts";
import { getErrorMessage } from "@/utils/base.ts";
import axios from "axios";
import { backendEndpoint } from "@/conf/env.ts";
export type TestWebSearchResponse = CommonResponse & {
result: string;
};
export type whiteList = {
enabled: boolean;
custom: string;
white_list: string[];
};
export type GeneralState = {
title: string;
logo: string;
description: string;
backend: string;
docs: string;
file: string;
pwa_manifest: string;
gravatar: string;
debug_mode: boolean;
realtime?: {
ws?: {
buffer_size?: number;
aggregate?: boolean;
aggregate_window_ms?: number;
};
};
};
export type MailState = {
host: string;
protocol: boolean;
port: number;
username: string;
password: string;
from: string;
white_list: whiteList;
};
export type SearchState = {
endpoint: string;
crop: boolean;
crop_len: number;
engines: string[];
image_proxy: boolean;
safe_search: number;
llm_extract: boolean;
llm_model: string;
};
export type SecurityState = {
check_type: string;
check_models?: string[];
text_database: string;
regex_database: string;
baidu_api_key: string;
baidu_secret_key: string;
custom_endpoint: string;
custom_audit_token: string;
blacklist_ips: string[];
whitelist_ips: string[];
};
export type PaymentState = {
stripe: {
enabled: boolean;
public_key: string;
secret_key: string;
webhook_secret: string;
};
epay: {
domain: string;
business_id: string;
business_key: string;
enabled: boolean;
methods: string[];
aggregation: boolean;
};
wechatpay?: {
enabled: boolean;
app_id: string;
mch_id: string;
serial_no: string;
apiv3_key: string;
wechatcertificate: string;
};
xunhupay?: {
wechat_enabled: boolean;
alipay_enabled: boolean;
wechat_app_id: string;
wechat_app_secret: string;
alipay_app_id: string;
alipay_app_secret: string;
endpoint: string;
};
affiliate?: {
enabled: boolean;
commission_rate: number;
min_withdraw: number;
allow_existing_bind: boolean;
};
};
export type SiteState = {
close_register: boolean;
currency: string;
close_relay: boolean;
relay_plan: boolean;
quota: number;
buy_link: string;
announcement: string;
contact: string;
footer: string;
auth_footer: boolean;
pre_deduct_quota: boolean;
hide_key_docs: boolean;
};
export type CustomState = {
custom_js: string;
custom_css: string;
custom_html: string;
ga_tracking_id: string;
};
export type AutoTitleState = {
enabled: boolean;
model: string;
max_len: number;
min_msgs: number;
overwrite: boolean;
prompt: string;
};
export type CommonState = {
cache: string[];
expire: number;
size: number;
article: string[];
generation: string[];
prompt_store: boolean;
image_store: boolean;
};
export type SystemProps = {
general: GeneralState;
site: SiteState;
mail: MailState;
search: SearchState;
common: CommonState;
payment: PaymentState;
security: SecurityState;
custom: CustomState;
auto_title?: AutoTitleState;
};
export type SystemResponse = CommonResponse & {
data?: SystemProps;
};
export const initialSystemState: SystemProps = {
general: {
logo: "",
description: "",
title: "",
backend: "",
docs: "",
file: "",
pwa_manifest: "",
gravatar: "",
debug_mode: false,
realtime: {
ws: {
buffer_size: 24,
aggregate: true,
aggregate_window_ms: 20,
},
},
},
site: {
close_register: false,
currency: "cny",
close_relay: false,
relay_plan: false,
quota: 0,
buy_link: "",
announcement: "",
contact: "",
footer: "",
auth_footer: false,
pre_deduct_quota: true,
hide_key_docs: false,
},
mail: {
host: "",
protocol: false,
port: 465,
username: "",
password: "",
from: "",
white_list: {
enabled: false,
custom: "",
white_list: [],
},
},
search: {
endpoint: "",
crop: false,
crop_len: 1000,
engines: [],
image_proxy: false,
safe_search: 0,
llm_extract: false,
llm_model: "",
},
common: {
article: [],
generation: [],
cache: [],
expire: 3600,
size: 1,
prompt_store: false,
image_store: false,
},
payment: {
stripe: {
enabled: false,
public_key: "",
secret_key: "",
webhook_secret: "",
},
epay: {
domain: "",
business_id: "",
business_key: "",
enabled: false,
methods: [],
aggregation: false,
},
affiliate: {
enabled: false,
commission_rate: 0.1,
min_withdraw: 10,
allow_existing_bind: false,
},
},
security: {
check_type: "",
check_models: [],
text_database: "",
regex_database: "",
baidu_api_key: "",
baidu_secret_key: "",
custom_endpoint: "",
custom_audit_token: "",
blacklist_ips: [],
whitelist_ips: [],
},
custom: {
custom_js: "",
custom_css: "",
custom_html: "",
ga_tracking_id: "",
},
auto_title: {
enabled: false,
model: "",
max_len: 50,
min_msgs: 6,
overwrite: false,
prompt: "",
},
};
export async function getConfig(): Promise {
try {
const response = await axios.get("/admin/config/view");
const data = response.data as SystemResponse;
if (data.status && data.data) {
// init system data pre-format
data.data.mail.white_list.white_list =
data.data.mail.white_list.white_list || commonWhiteList;
data.data.search.engines = data.data.search.engines || [];
data.data.search.crop_len =
data.data.search.crop_len && data.data.search.crop_len > 0
? data.data.search.crop_len
: 1000;
data.data.site.currency = data.data.site.currency || "cny";
if (
!data.data.common.group ||
Object.keys(data.data.common.group).length === 0
) {
data.data.common.group = {
anonymous: {
buy_price: 1,
consume_price: 1,
description: "",
},
normal: {
buy_price: 1,
consume_price: 1,
description: "",
},
basic: {
buy_price: 1,
consume_price: 1,
description: "",
},
standard: {
buy_price: 1,
consume_price: 1,
description: "",
},
pro: {
buy_price: 1,
consume_price: 1,
description: "",
},
admin: {
buy_price: 1,
consume_price: 1,
description: "",
},
};
}
const rt = (data.data.general.realtime = data.data.general.realtime || {});
const ws = (rt.ws = rt.ws || {});
ws.buffer_size = typeof ws.buffer_size === "number" && ws.buffer_size > 0 ? ws.buffer_size : 1;
ws.aggregate = typeof ws.aggregate === "boolean" ? ws.aggregate : true;
ws.aggregate_window_ms = typeof ws.aggregate_window_ms === "number" && ws.aggregate_window_ms > 0 ? ws.aggregate_window_ms : 20;
const at = (data.data.auto_title = data.data.auto_title || {
enabled: false,
model: "",
max_len: 50,
min_msgs: 6,
overwrite: false,
prompt: "",
});
at.enabled = !!at.enabled;
at.model = at.model || "";
at.max_len = typeof at.max_len === "number" && at.max_len > 0 ? at.max_len : 50;
at.min_msgs = typeof at.min_msgs === "number" && at.min_msgs > 0 ? at.min_msgs : 6;
at.overwrite = !!at.overwrite;
at.prompt = at.prompt || "";
}
return data;
} catch (e) {
return { status: false, error: getErrorMessage(e) };
}
}
export async function setConfig(config: SystemProps): Promise {
try {
const response = await axios.post(`/admin/config/update`, config);
return response.data as CommonResponse;
} catch (e) {
return { status: false, error: getErrorMessage(e) };
}
}
type UploadResponse = CommonResponse & {
url?: string;
};
export async function uploadFavicon(file: File): Promise {
try {
const formData = new FormData();
formData.append("file", file);
const response = await axios.post(`/admin/favicon/upload`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
return response.data as UploadResponse;
} catch (e) {
return { status: false, error: getErrorMessage(e) };
}
}
export async function uploadResource(file: File): Promise {
try {
const formData = new FormData();
formData.append("file", file);
const response = await axios.post(`/admin/resource/upload`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
const data = response.data as UploadResponse;
if (data.status) {
data.url = backendEndpoint + data.url;
}
return data;
} catch (e) {
return { status: false, error: getErrorMessage(e) };
}
}
export async function updateRootPassword(
password: string,
): Promise {
try {
const response = await axios.post(`/admin/user/root`, { password });
return response.data as CommonResponse;
} catch (e) {
return { status: false, error: getErrorMessage(e) };
}
}
export async function testWebSearching(
query: string,
): Promise {
try {
const response = await axios.get(
`/admin/config/test/search?query=${encodeURIComponent(query)}`,
);
return response.data as TestWebSearchResponse;
} catch (e) {
return { status: false, error: getErrorMessage(e), result: "" };
}
}
export enum AuditTypes {
None = "none",
Dict = "dict",
Regex = "regex",
Baidu = "baidu",
Custom = "custom",
}
export const auditTypes: string[] = [
AuditTypes.None,
AuditTypes.Dict,
AuditTypes.Regex,
AuditTypes.Baidu,
AuditTypes.Custom,
];
export const commonWhiteList: string[] = [
"gmail.com",
"outlook.com",
"yahoo.com",
"hotmail.com",
"foxmail.com",
"icloud.com",
"qq.com",
"163.com",
"126.com",
];
================================================
FILE: app/src/admin/channel.ts
================================================
import { getUniqueList } from "@/utils/base.ts";
import {
AnonymousType,
BasicType,
NormalType,
ProType,
StandardType,
} from "@/utils/groups.ts";
export type Channel = {
id: number;
name: string;
type: string;
models: string[];
priority: number;
weight: number;
retry: number;
secret: string;
endpoint: string;
mapper: string;
state: boolean;
group?: string[];
proxy?: {
proxy: string;
proxy_type: number;
username: string;
password: string;
};
first_message_as_user?: boolean;
merge_consecutive_user_messages?: boolean;
};
export enum proxyType {
NoneProxy = 0,
HttpProxy = 1,
HttpsProxy = 2,
Socks5Proxy = 3,
}
export const ProxyTypes: Record = {
[proxyType.NoneProxy]: "None Proxy",
[proxyType.HttpProxy]: "HTTP Proxy",
[proxyType.HttpsProxy]: "HTTPS Proxy",
[proxyType.Socks5Proxy]: "SOCKS5 Proxy",
};
export type ChannelInfo = {
description?: string;
endpoint: string;
format: string;
models: string[];
};
export const ChannelTypes: Record = {
openai: "OpenAI",
azure: "Azure OpenAI",
claude: "Anthropic Claude",
palm: "Google Gemini",
midjourney: "Midjourney Proxy",
sparkdesk: "讯飞星火 SparkDesk",
chatglm: "智谱清言 ChatGLM",
moonshot: "月之暗面 Moonshot",
qwen: "通义千问 TongYi",
hunyuan: "腾讯混元 Hunyuan",
zhinao: "360智脑 360GLM",
baichuan: "百川大模型 BaichuanAI",
skylark: "云雀大模型 SkylarkLLM",
groq: "Groq Cloud",
bing: "New Bing",
slack: "Slack Claude",
deepseek: "深度求索 DeepSeek",
coze: "扣子 Coze",
dify: "Dify",
};
export const ShortChannelTypes: Record = {
openai: "OpenAI",
azure: "Azure",
claude: "Claude",
palm: "Gemini",
midjourney: "Midjourney",
sparkdesk: "讯飞星火",
chatglm: "ChatGLM",
moonshot: "Moonshot",
qwen: "通义千问",
hunyuan: "腾讯混元",
zhinao: "360 智脑",
baichuan: "百川 AI",
skylark: "火山方舟",
groq: "Groq",
bing: "Bing",
slack: "Slack",
deepseek: "DeepSeek",
coze: "Coze",
dify: "Dify",
};
export const ChannelInfos: Record = {
openai: {
endpoint: "https://api.openai.com",
format: "",
models: [
"gpt-3.5-turbo",
"gpt-3.5-turbo-instruct",
"gpt-3.5-turbo-0613",
"gpt-3.5-turbo-0301",
"gpt-3.5-turbo-1106",
"gpt-3.5-turbo-0125",
"gpt-3.5-turbo-16k",
"gpt-3.5-turbo-16k-0613",
"gpt-3.5-turbo-16k-0301",
"gpt-4",
"gpt-4-0314",
"gpt-4-0613",
"gpt-4-1106-preview",
"gpt-4-0125-preview",
"gpt-4-turbo-preview",
"gpt-4-vision-preview",
"gpt-4-1106-vision-preview",
"gpt-4-turbo",
"gpt-4-turbo-2024-04-09",
"gpt-4-32k",
"gpt-4-32k-0314",
"gpt-4-32k-0613",
"gpt-4o",
"gpt-4o-2024-05-13",
"gpt-4o-mini",
"gpt-4o-2024-08-06",
"gpt-4o-mini-2024-07-18",
"dalle",
"dall-e-2",
"dall-e-3",
],
},
azure: {
endpoint: "2023-12-01-preview",
format: "|",
description:
"> Azure 密钥 API Key 1 和 API Key 2 任填一个即可,密钥格式为 **api-key|api-endpoint**, api-endpoint 为 Azure 的 **API 端点**。\n" +
"> 接入点填 **API Version**,如 2023-12-01-preview。\n" +
"Azure 模型名称忽略点号等问题内部已经进行适配,无需额外任何设置。",
models: [
"gpt-3.5-turbo",
"gpt-3.5-turbo-instruct",
"gpt-3.5-turbo-0613",
"gpt-3.5-turbo-0301",
"gpt-3.5-turbo-1106",
"gpt-3.5-turbo-0125",
"gpt-3.5-turbo-16k",
"gpt-3.5-turbo-16k-0613",
"gpt-3.5-turbo-16k-0301",
"gpt-4",
"gpt-4-0314",
"gpt-4-0613",
"gpt-4-1106-preview",
"gpt-4-0125-preview",
"gpt-4-turbo-preview",
"gpt-4-vision-preview",
"gpt-4-1106-vision-preview",
"gpt-4-turbo",
"gpt-4-turbo-2024-04-09",
"gpt-4-32k",
"gpt-4-32k-0314",
"gpt-4-32k-0613",
"dalle",
"dall-e-2",
"dall-e-3",
],
},
claude: {
endpoint: "https://api.anthropic.com",
format: "",
description:
"> Anthropic Claude 密钥即为 **x-api-key**,Anthropic 对请求 IP 地域有限制,可能出现 **Request not allowed** 的错误,请尝试更换 IP 或者使用代理。\n",
models: [
"claude-instant-1.2",
"claude-2",
"claude-2.1",
"claude-3-opus-20240229",
"claude-3-sonnet-20240229",
"claude-3-haiku-20240307",
],
},
slack: {
endpoint: "your-channel",
format: "|",
models: ["claude-slack"],
description:
"> **注意!当前个人免费版 Slack 已不支持 Claude 调用。** \n" +
"> 密钥请填写 bot-id|xoxp-token,其中 bot-id 为 Slack Bot 的 ID,xoxp-token 为 Slack Bot 的 xoxp-token \n" +
"> 接入点填写你的 Slack Channel 名称,如 *chatnio* \n" +
"> 详情参考 [claude-api](https://github.com/bincooo/claude-api) \n",
},
sparkdesk: {
endpoint: "wss://spark-api.xf-yun.com",
format: "||",
models: [
"spark-desk-v1.5",
"spark-desk-v2",
"spark-desk-v3",
"spark-desk-v3.5",
],
},
chatglm: {
endpoint: "https://open.bigmodel.cn",
format: "",
models: ["glm-4", "glm-4v", "glm-3-turbo"],
description:
"> 智谱 ChatGLM 密钥格式为 **api-key**,接入点填写 *https://open.bigmodel.cn* \n",
},
qwen: {
endpoint: "https://dashscope.aliyuncs.com",
format: "",
models: ["qwen-turbo", "qwen-plus", "qwen-turbo-net", "qwen-plus-net"],
},
hunyuan: {
endpoint: "https://hunyuan.cloud.tencent.com",
format: "||",
models: ["hunyuan"],
// endpoint
},
zhinao: {
endpoint: "https://api.360.cn",
format: "",
models: ["360-gpt-v9"],
},
baichuan: {
endpoint: "https://api.baichuan-ai.com",
format: "",
models: ["baichuan-53b"],
},
skylark: {
endpoint: "https://ark.cn-beijing.volces.com/api/v3",
format: "|",
models: [
"skylark-lite-public",
"skylark-plus-public",
"skylark-pro-public",
"skylark-chat",
],
description:
"> Skylark 格式密钥请填写获取到的 ak|sk 或 apikey \n" +
"> 接入点填写生成的接入点,如 *https://ark.cn-beijing.volces.com/api/v3* \n" +
"> Skylark API 的地域字段无需手动填写,系统会自动根据接入点获取 \n",
},
bing: {
endpoint: "wss://your.bing.service",
format: "",
models: ["bing-creative", "bing-balanced", "bing-precise"],
description:
"> New Bing 服务搭建详情请参考 [chatnio-bing-service](https://github.com/coaidev/chatnio-bing-service) \n " +
"> bing2api (如 [bingo](https://github.com/weaigc/bingo)) 可直接使用 **OpenAI** 格式而非 **New Bing** 格式 \n " +
"> 接入点填写你部署的站点即可,如 *http://localhost:8765* ",
},
palm: {
endpoint: "https://generativelanguage.googleapis.com",
format: "",
models: [
"chat-bison-001",
"gemini-pro",
"gemini-pro-vision",
"gemini-1.5-pro-latest",
"gemini-1.5-flash-latest",
],
description:
"> Google Gemini / PaLM2 密钥格式为 **api-key**,接入点填写 *https://generativelanguage.googleapis.com* 或其反代地址 \n" +
"> Google 对请求 IP 地域有限制,可能出现 **User Location Is Not Supported** 的错误,可以看运气通过反代解决。 \n" +
"> Gemini Pro 的返回结果一次性而非流式(即使 `streamGenerateContent` 接口也为假流式),系统内部做了平滑伪流式处理,但仍然无法从根本解决 Gemini Pro 自身假流式的特性。\n",
},
midjourney: {
endpoint: "https://your.midjourney.proxy",
format: "|",
models: ["midjourney", "midjourney-fast", "midjourney-turbo"],
description:
"> 请参考 [midjourney-proxy](https://github.com/novicezk/midjourney-proxy) 项目填入参数,可设置白名单 *white-list* 以限制回调 IP \n" +
"> 密钥举例: password|localhost,127.0.0.1,196.128.0.31\n" +
"> 密钥即为 *mj-api-secret* (如果没有设置 secret 请填 `null` ) \n" +
"> 白名单即为 *white-list*(如果没有回调 IP 白名单默认接收所有 IP 的回调,不需要加 | 以及后面的内容) \n" +
"> 接入点填写你的 Midjourney Proxy 的部署地址,如 *http://localhost:8080*, *https://example.com* \n" +
"> 注意:**请在系统设置中设置后端的公网 IP / 域名,否则无法接收回调报错 please provide available notify url** \n",
},
moonshot: {
endpoint: "https://api.moonshot.cn",
format: "",
models: ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"],
},
groq: {
endpoint: "https://api.groq.com/openai",
format: "",
models: ["llama2-70b-4096", "mixtral-8x7b-32768", "gemma-7b-it"],
},
deepseek: {
endpoint: "https://api.deepseek.com",
format: "",
models: ["deepseek-chat", "deepseek-reasoner"],
},
coze: {
endpoint: "https://api.coze.cn",
format: "",
models: [""],
description:
"> 扣子 Coze 的模型名称即为 Coze 平台的 **bot_id** \n" +
"> 进入智能体的开发页面,开发页面 URL 中 bot 参数后的数字就是智能体 ID \n" +
"> 例如 [https://www.coze.cn/space/341****/bot/73428668*****](https://www.coze.cn/space/341****/bot/73428668*****),智能体 ID 为 73428668***** \n" +
"> 确保当前使用的访问密钥已被授予智能体所属空间的 chat 权限 \n" +
"> 如果需要让系统自动适配扣子 Coze 平台的图标,请在 **模型映射** 中将 **bot_id** 映射为 **coze** 开头的模型,如 coze-chat>73428668***** \n",
},
dify: {
endpoint: "https://api.dify.ai/v1",
format: "",
models: [""],
description:
"> 由于 Dify 平台一个 Key 对应一个 CHATFLOW (模型),所以模型名称仅在用户调用本系统时用于标识用户调用的对象,不代表调用 Dify 平台 CHATFLOW 时被调用 CHATFLOW 的名称 \n" +
"> 因此,您需要为每一个 Dify 平台的 CHATFLOW 分别创建渠道 \n" +
"> 如果需要让系统自动适配 Dify 平台的图标,请将模型名称填写为 **dify** 开头的模型,如 **dify-chat** \n",
},
};
export const defaultChannelModels: string[] = getUniqueList(
Object.values(ChannelInfos).flatMap((info) => info.models),
);
export const channelGroups: string[] = [
AnonymousType,
NormalType,
BasicType,
StandardType,
ProType,
];
export function getChannelInfo(type?: string): ChannelInfo {
if (type && type in ChannelInfos) return ChannelInfos[type];
return ChannelInfos.openai;
}
export function getChannelType(type?: string): string {
if (type && type in ChannelTypes) return ChannelTypes[type];
return ChannelTypes.openai;
}
export function getShortChannelType(type?: string): string {
if (type && type in ShortChannelTypes) return ShortChannelTypes[type];
return ShortChannelTypes.openai;
}
================================================
FILE: app/src/admin/charge.ts
================================================
export const tokenBilling = "token-billing";
export const timesBilling = "times-billing";
export const nonBilling = "non-billing";
export const defaultChargeType = tokenBilling;
export const chargeTypes = [nonBilling, timesBilling, tokenBilling];
export type ChargeType = (typeof chargeTypes)[number];
export type ChargeBaseProps = {
type: string;
anonymous: boolean;
input: number;
output: number;
};
export type ChargeProps = ChargeBaseProps & {
id: number;
models: string[];
};
================================================
FILE: app/src/admin/colors.ts
================================================
export const modelColorMapper: Record = {
// OpenAI & Azure OpenAI
"gpt-3.5-turbo": "green-500",
"gpt-3.5-turbo-16k": "green-600",
"gpt-4": "purple-600",
dalle: "green-600",
"dall-e-2": "green-600",
"dall-e-3": "purple-700",
whisper: "gray-300",
tts: "gray-300",
openai: "gray-300",
azure: "gray-300",
// Anthropic Claude
"claude-3": "orange-500",
claude: "orange-400",
anthropic: "orange-400",
// Spark Desk
"spark-desk": "blue-400",
sparkdesk: "blue-400",
// Moonshot
moonshot: "black-500",
kimi: "black-500",
// Midjourney
midjourney: "indigo-600",
"mid-journey": "indigo-600",
niji: "indigo-600",
// Stable Diffusion
"stable-diffusion": "gray-400",
stablediffusion: "gray-400",
stability: "gray-400",
// Groq Cloud
"llama2-70b-4096": "red-500",
"mixtral-8x7b-32768": "red-500",
"gemma-7b-it": "red-500",
// Google Gemini & Gemma
"chat-bison-001": "red-500",
palm: "red-500",
gemini: "red-500",
gemma: "red-500",
// DeepSeek
deepseek: "blue-700",
// New Bing
bing: "blue-700",
// ChatGLM
zhipu: "lime-500",
glm: "lime-500",
// Tongyi Qwen
qwen: "indigo-600",
tongyi: "indigo-600",
// Meta LLaMA
llama: "sky-400",
// Tencent Hunyuan
hunyuan: "blue-500",
// 360 GPT
"360": "stone-500",
// Baichuan AI
baichuan: "orange-700",
// ByteDance Skylark / Doubao / Coze
skylark: "sky-300",
doubao: "sky-300",
coze: "sky-300",
// Dify
dify: "gray-300",
// OpenRouter
openrouter: "purple-600",
};
const unknownColors = [
"gray-700",
"indigo-600",
"green-500",
"green-600",
"purple-600",
"purple-700",
"orange-400",
"blue-400",
"red-500",
"blue-700",
"lime-500",
"sky-400",
"stone-500",
"orange-700",
"sky-300",
];
export function getUnknownModelColor(model: string): string {
const char = model.length > 0 ? model[0] : "A";
const code = char.charCodeAt(0);
return unknownColors[code % unknownColors.length];
}
export function getModelColor(model: string): string {
for (const key in modelColorMapper) {
if (model.toLowerCase().includes(key)) {
return modelColorMapper[key];
}
}
return getUnknownModelColor(model);
}
================================================
FILE: app/src/admin/datasets/charge.ts
================================================
import {
ChargeProps,
ChargeType,
timesBilling,
tokenBilling,
} from "@/admin/charge.ts";
export enum Currency {
CNY = "CNY",
USD = "USD",
}
export type PricingItem = {
models: string[];
input?: number;
output: number;
currency?: Currency;
billing_type?: ChargeType;
};
export type PricingDataset = PricingItem[];
export const pricing: PricingDataset = [
{
models: [
"gpt-3.5-turbo",
"gpt-3.5-turbo-0301",
"gpt-3.5-turbo-0613",
"gpt-3.5-turbo-instruct",
],
input: 0.0015,
output: 0.002,
},
{
models: ["gpt-3.5-turbo-1106"],
input: 0.001,
output: 0.002,
},
{
models: ["gpt-3.5-turbo-0125"],
input: 0.0005,
output: 0.0015,
},
{
models: [
"gpt-3.5-turbo-16k",
"gpt-3.5-turbo-16k-0301",
"gpt-3.5-turbo-16k-0613",
],
input: 0.003,
output: 0.004,
},
{
models: ["gpt-4", "gpt-4-0314", "gpt-4-0613"],
input: 0.03,
output: 0.06,
},
{
models: [
"gpt-4-1106-preview",
"gpt-4-0125-preview",
"gpt-4-turbo-preview",
"gpt-4-1106-vision-preview",
"gpt-4-vision-preview",
"gpt-4-turbo",
"gpt-4-turbo-2024-04-09",
],
input: 0.01,
output: 0.03,
},
{
models: ["gpt-4o", "gpt-4o-2024-05-13"],
input: 0.005,
output: 0.015,
},
{
models: ["gpt-4o-2024-08-06"],
input: 0.0025,
output: 0.01,
},
{
models: ["gpt-4o-mini", "gpt-4o-mini-2024-07-18"],
input: 0.00015,
output: 0.0006,
},
{
models: ["gpt-4-32k", "gpt-4-32k-0314", "gpt-4-32k-0613"],
input: 0.06,
output: 0.12,
},
{
models: ["dalle", "dall-e-2"], // dall-e-2 512x512 size
output: 0.018,
billing_type: timesBilling,
},
{
models: ["dall-e-3"], // dall-e-3 HD 1024x1024 size
output: 0.08,
billing_type: timesBilling,
},
{
models: [
"claude-1",
"claude-1-100k",
"claude-1.2",
"claude-1.3",
"claude-instant",
"claude-instant-1.2",
"claude-slack",
],
input: 0.0008,
output: 0.0024,
// input: $0.8/1m tokens, output: $2.4/1m tokens
},
{
models: ["claude-2", "claude-2-100k", "claude-2.1"],
input: 0.008,
output: 0.024,
},
// claude 3 haiku $0.25/1m tokens input & $1.25/1m tokens output
{
models: ["claude-3-haiku-20240307"],
input: 0.00025,
output: 0.00125,
},
// claude 3 sonnet $3/1m tokens input & $15/1m tokens output
{
models: ["claude-3-sonnet-20240229"],
input: 0.003,
output: 0.015,
},
// claude 3 sonnet $15/1m tokens input & $75/1m tokens output
{
models: ["claude-3-opus-20240229"],
input: 0.015,
output: 0.075,
},
{
models: ["midjourney"],
output: 0.1,
currency: Currency.CNY,
billing_type: timesBilling,
},
{
models: ["midjourney-fast"],
output: 0.2,
currency: Currency.CNY,
billing_type: timesBilling,
},
{
models: ["midjourney-turbo"],
output: 0.5,
currency: Currency.CNY,
billing_type: timesBilling,
},
{
models: ["spark-desk-v1.5"],
input: 0.015,
output: 0.015,
currency: Currency.CNY,
},
{
models: ["spark-desk-v2", "spark-desk-v3", "spark-desk-v3.5"],
input: 0.03,
output: 0.03,
currency: Currency.CNY,
},
{
models: ["moonshot-v1-8k"],
input: 0.012,
output: 0.012,
currency: Currency.CNY,
},
{
models: ["moonshot-v1-32k"],
input: 0.024,
output: 0.024,
currency: Currency.CNY,
},
{
models: ["moonshot-v1-128k"],
input: 0.06,
output: 0.06,
currency: Currency.CNY,
},
{
models: ["glm-4", "glm-4v"],
input: 0.1,
output: 0.1,
currency: Currency.CNY,
},
{
models: [
"zhipu-chatglm-lite",
"zhipu-chatglm-std",
"zhipu-chatglm-turbo",
"glm-3-turbo",
],
input: 0.005,
output: 0.005,
currency: Currency.CNY,
},
{
models: ["zhipu-chatglm-pro"],
input: 0.01,
output: 0.01,
currency: Currency.CNY,
},
{
models: ["qwen-plus", "qwen-plus-net"],
input: 0.02,
output: 0.02,
currency: Currency.CNY,
},
{
models: ["qwen-turbo", "qwen-turbo-net"],
input: 0.008,
output: 0.008,
currency: Currency.CNY,
},
{
models: ["chat-bison-001"], // free marked as $0.001
output: 0.001,
},
{
models: [
"gemini-pro",
"gemini-pro-vision",
"gemini-1.5-pro-latest",
"gemini-1.5-flash-latest",
],
input: 0.000125,
output: 0.000375,
},
{
models: ["hunyuan"],
input: 0.1,
output: 0.1,
currency: Currency.CNY,
},
{
models: ["deepseek-chat", "deepseek-coder"],
input: 0.001,
output: 0.002,
currency: Currency.CNY,
},
{
models: ["360-gpt-v9"],
input: 0.12,
output: 0.12,
currency: Currency.CNY,
},
{
models: ["baichuan-53b"],
input: 0.02,
output: 0.02,
currency: Currency.CNY,
},
{
models: ["skylark-lite-public"],
input: 0.004,
output: 0.004,
currency: Currency.CNY,
},
{
models: ["skylark-plus-public"],
input: 0.008,
output: 0.008,
currency: Currency.CNY,
},
{
models: ["skylark-pro-public", "skylark-chat"],
input: 0.011,
output: 0.011,
currency: Currency.CNY,
},
{
models: ["llama2-70b-4096", "mixtral-8x7b-32768", "gemma-7b-it"],
output: 0.001, // free marked as $0.001
currency: Currency.USD,
},
];
const countPricing = (
_price?: number,
_currency?: Currency,
usd?: number,
): number => {
const price = _price ?? 0;
const currency = _currency ?? Currency.USD;
switch (currency) {
case Currency.CNY:
return price * 10; // 1 cny = 10 quota
case Currency.USD:
return price * 10 * (usd ?? 1);
default:
return countPricing(price, Currency.USD, usd);
}
};
export const getPricing = (currency: number): ChargeProps[] =>
pricing.map(
(item, index): ChargeProps => ({
id: index,
models: item.models,
type: item.billing_type ?? tokenBilling,
anonymous: false,
input: countPricing(item.input, item.currency, currency),
output: countPricing(item.output, item.currency, currency),
}),
);
================================================
FILE: app/src/admin/hook.tsx
================================================
import { useMemo, useState } from "react";
import { getUniqueList } from "@/utils/base.ts";
import { defaultChannelModels } from "@/admin/channel.ts";
import { getApiMarket, getApiModels } from "@/api/v1.ts";
import { useEffectAsync } from "@/utils/hook.ts";
import { Model } from "@/api/types.tsx";
export type onStateChange = (state: boolean, data?: T) => void;
export const useAllModels = (onStateChange?: onStateChange) => {
const [allModels, setAllModels] = useState([]);
const update = async () => {
onStateChange?.(false, allModels);
const models = await getApiModels();
onStateChange?.(true, models.data);
setAllModels(models.data);
};
useEffectAsync(update, []);
return {
allModels,
update,
};
};
export const useChannelModels = (onStateChange?: onStateChange) => {
const { allModels, update } = useAllModels(onStateChange);
const channelModels = useMemo(
() => getUniqueList([...allModels, ...defaultChannelModels]),
[allModels],
);
return {
channelModels,
allModels,
update,
};
};
export const useSupportModels = (onStateChange?: onStateChange) => {
const [supportModels, setSupportModels] = useState([]);
const update = async () => {
onStateChange?.(false, supportModels);
const market = await getApiMarket();
onStateChange?.(true, market);
setSupportModels(market);
};
useEffectAsync(update, []);
return {
supportModels,
update,
};
};
================================================
FILE: app/src/admin/market.ts
================================================
export const marketEditableTags = [
"official",
"multi-modal",
"web",
"high-quality",
"high-price",
"open-source",
"image-generation",
"unstable",
];
export const deprecatedModelImages = [
"gpt35turbo.png",
"gpt35turbo16k.webp",
"gpt4.png",
"gpt432k.webp",
"gpt4v.png",
"gpt4dalle.png",
"claude.png",
"claude100k.png",
"stablediffusion.jpeg",
"llama2.webp",
"llamacode.webp",
"dalle.jpeg",
"midjourney.jpg",
"newbing.jpg",
"palm2.webp",
"gemini.jpeg",
"chatglm.png",
"tongyi.png",
"sparkdesk.jpg",
"hunyuan.png",
"360gpt.png",
"baichuan.png",
"skylark.jpg",
];
export const marketTags = ["high-context", ...marketEditableTags, "free"];
================================================
FILE: app/src/admin/types.ts
================================================
export type CommonResponse = {
status: boolean;
message: string;
error?: string;
};
export type InfoResponse = {
billing_today: number;
billing_yesterday: number;
billing_month: number;
billing_last_month: number;
subscription_count: number;
online_chats: number;
};
export type ModelChartResponse = {
date: string[];
value: {
model: string;
data: number[];
}[];
};
export type RequestChartResponse = {
date: string[];
value: number[];
};
export type BillingChartResponse = {
date: string[];
value: number[];
};
export type ErrorChartResponse = {
date: string[];
value: number[];
};
export type UserTypeChartResponse = {
total: number;
normal: number;
api_paid: number;
basic_plan: number;
standard_plan: number;
pro_plan: number;
};
export type InvitationData = {
code: string;
quota: number;
type: string;
used: boolean;
username: string;
created_at: string;
updated_at: string;
};
export type InvitationForm = {
data: InvitationData[];
total: number;
};
export type InvitationResponse = {
status: boolean;
message: string;
data: InvitationData[];
total: number;
};
export type Redeem = {
code: string;
quota: number;
used: boolean;
created_at: string;
updated_at: string;
};
export type RedeemForm = {
data: Redeem[];
total: number;
};
export type RedeemResponse = CommonResponse & {
data: Redeem[];
total: number;
};
export type InvitationGenerateResponse = {
status: boolean;
data: string[];
message: string;
};
export type RedeemGenerateResponse = {
status: boolean;
data: string[];
message: string;
};
export type UserData = {
id: number;
username: string;
email: string;
is_banned: boolean;
is_admin: boolean;
quota: number;
used_quota: number;
is_subscribed: boolean;
total_month: number;
expired_at: string;
level: number;
enterprise: boolean;
};
export type UserForm = {
data: UserData[];
total: number;
};
export type UserResponse = {
status: boolean;
message: string;
data: UserData[];
total: number;
};
================================================
FILE: app/src/api/addition.ts
================================================
import axios from "axios";
import { getErrorMessage } from "@/utils/base.ts";
type QuotaResponse = {
status: boolean;
error: string;
};
type PackageResponse = {
status: boolean;
cert: boolean;
teenager: boolean;
};
type SubscriptionResponse = {
status: boolean;
is_subscribed: boolean;
expired: number;
enterprise?: boolean;
usage: Record;
level: number;
expired_at?: string;
refresh?: number;
refresh_at?: string;
};
type BuySubscriptionResponse = {
status: boolean;
error: string;
};
type ApiKeyResponse = {
status: boolean;
key: string;
};
type ResetApiKeyResponse = {
status: boolean;
key: string;
error: string;
};
export async function buyQuota(quota: number): Promise {
try {
const resp = await axios.post(`/buy`, { quota });
return resp.data as QuotaResponse;
} catch (e) {
console.debug(e);
return { status: false, error: "network error" };
}
}
export async function getPackage(): Promise {
try {
const resp = await axios.get(`/package`);
if (resp.data.status === false) {
return { status: false, cert: false, teenager: false };
}
return {
status: resp.data.status,
cert: resp.data.data.cert,
teenager: resp.data.data.teenager,
};
} catch (e) {
console.debug(e);
return { status: false, cert: false, teenager: false };
}
}
export async function getSubscription(): Promise {
try {
const resp = await axios.get(`/subscription`);
if (resp.data.status === false) {
return {
status: false,
is_subscribed: false,
level: 0,
expired: 0,
usage: {},
};
}
return resp.data as SubscriptionResponse;
} catch (e) {
console.debug(e);
return {
status: false,
is_subscribed: false,
level: 0,
expired: 0,
usage: {},
};
}
}
export async function buySubscription(
month: number,
level: number,
): Promise {
try {
const resp = await axios.post(`/subscribe`, { level, month });
return resp.data as BuySubscriptionResponse;
} catch (e) {
console.debug(e);
return { status: false, error: "network error" };
}
}
export async function getKey(): Promise {
try {
const resp = await axios.get(`/apikey`);
if (resp.data.status === false) {
return { status: false, key: "" };
}
return {
status: resp.data.status,
key: resp.data.key,
};
} catch (e) {
console.debug(e);
return { status: false, key: "" };
}
}
export async function regenerateKey(): Promise {
try {
const resp = await axios.post(`/resetkey`);
return resp.data as ResetApiKeyResponse;
} catch (e) {
console.debug(e);
return { status: false, key: "", error: getErrorMessage(e) };
}
}
================================================
FILE: app/src/api/auth.ts
================================================
import axios from "axios";
import { getErrorMessage } from "@/utils/base.ts";
import { isEmailValid } from "@/utils/form.ts";
import { toast } from "sonner";
export type LoginForm = {
username: string;
password: string;
};
export type DeepLoginForm = {
token: string;
};
export type LoginResponse = {
status: boolean;
error: string;
token: string;
};
export type StateResponse = {
status: boolean;
user: string;
admin: boolean;
};
export type RegisterForm = {
username: string;
password: string;
repassword: string;
email: string;
code: string;
};
export type RegisterResponse = {
status: boolean;
error: string;
token: string;
};
export type VerifyForm = {
email: string;
};
export type VerifyResponse = {
status: boolean;
error: string;
};
export type ResetForm = {
email: string;
code: string;
password: string;
repassword: string;
};
export type ResetResponse = {
status: boolean;
error: string;
};
export type UserInfo = {
id: number;
register_days: number;
used_quota: number;
plan_total_month: number;
email: string;
};
export type UserInfoResponse = {
status: boolean;
error: string;
data: UserInfo;
};
export async function doLogin(
data: DeepLoginForm | LoginForm,
): Promise {
const response = await axios.post("/login", data);
return response.data as LoginResponse;
}
export async function doState(): Promise {
const response = await axios.post("/state");
return response.data as StateResponse;
}
export async function doRegister(
data: RegisterForm,
): Promise {
try {
const response = await axios.post("/register", data);
return response.data as RegisterResponse;
} catch (e) {
return {
status: false,
error: getErrorMessage(e),
token: "",
};
}
}
export async function doVerify(
email: string,
checkout?: boolean,
): Promise {
try {
const response = await axios.post("/verify", {
email,
checkout,
} as VerifyForm);
return response.data as VerifyResponse;
} catch (e) {
return {
status: false,
error: getErrorMessage(e),
};
}
}
export async function doReset(data: ResetForm): Promise {
try {
const response = await axios.post("/reset", data);
return response.data as ResetResponse;
} catch (e) {
return {
status: false,
error: getErrorMessage(e),
};
}
}
export async function sendCode(
t: any,
email: string,
checkout?: boolean,
): Promise {
if (email.trim().length === 0 || !isEmailValid(email)) return false;
const res = await doVerify(email, checkout);
if (!res.status)
toast.error(t("auth.send-code-failed"), {
description: t("auth.send-code-failed-prompt", { reason: res.error }),
});
else
toast.info(t("auth.send-code-success"), {
description: t("auth.send-code-success-prompt"),
});
return res.status;
}
export const initialUserInfo: UserInfo = {
id: 0,
register_days: 0,
used_quota: 0,
plan_total_month: 0,
email: "",
};
export async function getUserInfo(): Promise {
try {
const response = await axios.get("/userinfo");
return response.data as UserInfoResponse;
} catch (e) {
return {
status: false,
error: getErrorMessage(e),
data: { ...initialUserInfo },
};
}
}
================================================
FILE: app/src/api/broadcast.ts
================================================
import axios from "axios";
import { getMemory, setMemory } from "@/utils/memory.ts";
export type Broadcast = {
content: string;
index: number;
};
export type BroadcastInfo = Broadcast & {
poster: string;
created_at: string;
};
export type BroadcastListResponse = {
data: BroadcastInfo[];
};
export type CommonBroadcastResponse = {
status: boolean;
error: string;
};
export async function getRawBroadcast(): Promise {
try {
const data = await axios.get("/broadcast/view");
if (data.data) return data.data as Broadcast;
} catch (e) {
console.warn(e);
}
return {
content: "",
index: 0,
};
}
export type BroadcastEvent = {
message: string;
firstReceived: boolean;
};
export async function getBroadcast(): Promise {
const data = await getRawBroadcast();
const content = data.content.trim();
if (content.length === 0)
return {
message: "",
firstReceived: false,
};
const memory = getMemory("broadcast");
if (memory === content)
return {
message: content,
firstReceived: false,
};
setMemory("broadcast", content);
return {
message: content,
firstReceived: true,
};
}
export async function getBroadcastList(): Promise {
try {
const resp = await axios.get("/broadcast/list");
const data = resp.data as BroadcastListResponse;
return data.data || [];
} catch (e) {
console.warn(e);
return [];
}
}
export async function createBroadcast(
content: string,
notify_all?: boolean,
): Promise {
try {
const resp = await axios.post("/broadcast/create", {
content,
notify_all,
});
return resp.data as CommonBroadcastResponse;
} catch (e) {
console.warn(e);
return {
status: false,
error: (e as Error).message,
};
}
}
export async function removeBroadcast(
index: number,
): Promise {
try {
const resp = await axios.post(`/broadcast/remove/${index}`);
return resp.data as CommonBroadcastResponse;
} catch (e) {
console.warn(e);
return {
status: false,
error: (e as Error).message,
};
}
}
export async function updateBroadcast(
id: number,
content: string,
): Promise {
try {
const resp = await axios.post("/broadcast/update", { id, content });
return resp.data as CommonBroadcastResponse;
} catch (e) {
console.warn(e);
return {
status: false,
error: (e as Error).message,
};
}
}
================================================
FILE: app/src/api/common.ts
================================================
import { toast } from "sonner";
export type CommonResponse = {
status: boolean;
error?: string;
reason?: string;
message?: string;
data?: any;
};
export function withNotify(
t: any,
state: CommonResponse,
toastSuccess?: boolean,
toastSuccessMessage?: string,
) {
if (state.status)
toastSuccess &&
toast.success(t("success"), {
description: toastSuccessMessage || t("request-success"),
});
else
toast.error(t("error"), {
description:
state.error ?? state.reason ?? state.message ?? "error occurred",
});
}
================================================
FILE: app/src/api/connection.ts
================================================
import { tokenField, websocketEndpoint } from "@/conf/bootstrap.ts";
import { getMemory } from "@/utils/memory.ts";
import { getErrorMessage } from "@/utils/base.ts";
import { Mask } from "@/masks/types.ts";
export const endpoint = `${websocketEndpoint}/chat`;
export const maxRetry = 60; // 30s max websocket retry
export const maxConnection = 5;
export type StreamMessage = {
conversation?: number;
keyword?: string;
quota?: number;
message: string;
end: boolean;
plan?: boolean;
title?: string;
search_query?: {
type: string;
search_queries: string[];
};
search_result?: {
type: string;
search_results: Array<{
url: string;
title: string;
snippet: string;
published_at?: number;
site_name?: string;
site_icon?: string;
}>;
};
search_index?: {
type: string;
search_indexes: Array<{
url: string;
cite_index: number;
}>;
};
tool_call?: {
name: string;
arguments?: unknown;
result?: string;
error?: string;
status: "start" | "executing" | "success" | "error";
};
response_type?: string;
};
export type ChatProps = {
type?: string;
message: string;
model: string;
web?: boolean;
web_search_mode?: "quick" | "detailed";
web_page_summary?: boolean;
think?: boolean;
context?: number;
ignore_context?: boolean;
// mcp related fields
enable_mcp?: boolean;
mcp_plugin_id?: number;
max_tokens?: number;
temperature?: number;
top_p?: number;
top_k?: number;
presence_penalty?: number;
frequency_penalty?: number;
repetition_penalty?: number;
};
type StreamCallback = (id: number, message: StreamMessage) => void;
export class Connection {
protected connection?: WebSocket;
protected callback?: StreamCallback;
protected stack?: Record;
public id: number;
public state: boolean;
public constructor(id: number, callback?: StreamCallback) {
this.state = false;
this.id = id;
callback && this.setCallback(callback);
}
public init(): void {
this.connection = new WebSocket(endpoint);
this.state = false;
this.connection.onopen = () => {
this.state = true;
this.send({
token: getMemory(tokenField) || "anonymous",
id: this.id,
});
};
this.connection.onclose = (event) => {
this.state = false;
this.stack = {
error: "websocket connection failed",
code: event.code,
reason: event.reason,
endpoint: endpoint,
};
setTimeout(() => {
console.debug(`[connection] reconnecting... (id: ${this.id})`);
this.init();
}, 3000);
};
this.connection.onmessage = (event) => {
const message = JSON.parse(event.data);
this.triggerCallback(message as StreamMessage);
};
}
public reconnect(): void {
this.init();
}
public send(data: Record): boolean {
if (!this.state || !this.connection) {
if (this.connection === undefined) this.init();
console.debug("[connection] connection not ready, retrying in 500ms...");
return false;
}
this.connection.send(JSON.stringify(data));
return true;
}
public sendWithRetry(t: any, data: ChatProps, times?: number): void {
try {
if (!times || times < maxRetry) {
if (!this.send(data)) {
setTimeout(() => {
this.sendWithRetry(t, data, (times ?? 0) + 1);
}, 500);
}
return;
}
} catch (e) {
console.warn(
`[connection] failed to send message: ${getErrorMessage(e)}`,
);
}
const trace = JSON.stringify(
this.stack ?? {
message: data.message,
endpoint: endpoint,
},
null,
2,
);
this.stack = undefined;
t &&
this.triggerCallback({
message: `${t("request-failed")}\n\`\`\`json\n${trace}\n\`\`\`\n`,
end: true,
});
}
public sendEvent(t: any, event: string, data?: string, props?: ChatProps) {
this.sendWithRetry(t, {
type: event,
message: data || "",
model: "event",
...props,
});
}
public sendStopEvent(t: any) {
this.sendEvent(t, "stop");
}
public sendRestartEvent(t: any, data?: ChatProps) {
this.sendEvent(t, "restart", undefined, data);
}
public sendMaskEvent(t: any, mask: Mask) {
this.sendEvent(t, "mask", JSON.stringify(mask.context));
}
public sendEditEvent(t: any, id: number, message: string) {
this.sendEvent(t, "edit", `${id}:${message}`);
}
public sendRemoveEvent(t: any, id: number) {
this.sendEvent(t, "remove", id.toString());
}
public sendShareEvent(t: any, refer: string) {
this.sendEvent(t, "share", refer);
}
public close(): void {
if (!this.connection) return;
this.connection.close();
}
public setCallback(callback?: StreamCallback): void {
this.callback = callback;
}
protected triggerCallback(message: StreamMessage): void {
this.callback && this.callback(this.id, message);
}
public setId(id: number): void {
this.id = id;
}
public isReady(): boolean {
return this.state;
}
public isRunning(): boolean {
if (!this.connection || !this.state) return false;
return this.connection.readyState === WebSocket.OPEN;
}
}
export class ConnectionStack {
protected connections: Connection[];
protected callback?: StreamCallback;
public constructor(callback?: StreamCallback) {
this.connections = [];
this.callback = callback;
}
public getConnection(id: number): Connection | undefined {
return this.connections.find((conn) => conn.id === id);
}
public createConnection(id: number): Connection {
const conn = new Connection(id, this.triggerCallback.bind(this));
this.connections.push(conn);
// max connection garbage collection
if (this.connections.length > maxConnection) {
const garbage = this.connections.shift();
garbage && garbage.close();
}
return conn;
}
public send(id: number, t: any, props: ChatProps) {
const conn = this.getConnection(id);
if (!conn) return false;
conn.sendWithRetry(t, props);
return true;
}
public hasConnection(id: number): boolean {
return this.connections.some((conn) => conn.id === id);
}
public setCallback(callback?: StreamCallback): void {
this.callback = callback;
}
public sendEvent(id: number, t: any, event: string, data?: string) {
const conn = this.getConnection(id);
conn && conn.sendEvent(t, event, data);
}
public sendStopEvent(id: number, t: any) {
const conn = this.getConnection(id);
conn && conn.sendStopEvent(t);
}
public sendRestartEvent(id: number, t: any, data?: ChatProps) {
const conn = this.getConnection(id);
conn && conn.sendRestartEvent(t, data);
}
public sendMaskEvent(id: number, t: any, mask: Mask) {
const conn = this.getConnection(id);
conn && conn.sendMaskEvent(t, mask);
}
public sendEditEvent(id: number, t: any, messageId: number, message: string) {
const conn = this.getConnection(id);
conn && conn.sendEditEvent(t, messageId, message);
}
public sendRemoveEvent(id: number, t: any, messageId: number) {
const conn = this.getConnection(id);
conn && conn.sendRemoveEvent(t, messageId);
}
public sendShareEvent(id: number, t: any, refer: string) {
const conn = this.getConnection(id);
conn && conn.sendShareEvent(t, refer);
}
public close(id: number): void {
const conn = this.getConnection(id);
conn && conn.close();
}
public closeAll(): void {
this.connections.forEach((conn) => conn.close());
}
public reconnect(id: number): void {
const conn = this.getConnection(id);
conn && conn.reconnect();
}
public reconnectAll(): void {
this.connections.forEach((conn) => conn.reconnect());
}
public raiseConnection(id: number): void {
const conn = this.getConnection(-1);
if (!conn) return;
conn.setId(id);
}
public triggerCallback(id: number, message: StreamMessage): void {
this.callback && this.callback(id, message);
}
}
================================================
FILE: app/src/api/file.ts
================================================
import { blobEndpoint } from "@/conf/env.ts";
import { trimSuffixes } from "@/utils/base.ts";
export type BlobParserResponse = {
status: boolean;
content: string;
error?: string;
};
export type FileObject = {
name: string;
content: string;
size?: number;
};
type Model = {
id: string;
ocr_model?: boolean;
vision_model?: boolean;
reverse_model?: boolean;
};
export type FileArray = FileObject[];
export async function fileToBase64(file: File): Promise {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = () => reject(new Error("Failed to read file"));
});
}
export function checkFileSuffix(
filename: string,
suffixes: string | string[],
): boolean {
filename = filename.toLowerCase();
if (typeof suffixes === "string") {
return filename.endsWith(suffixes);
}
return suffixes.some((suffix) => filename.endsWith(suffix));
}
export async function quickBlobParser(
file: File,
model: Model,
onProgress?: (progress: number) => void,
): Promise {
// this function is used to parse the file quickly in local
// otherwise, it will be parsed as a file
if (file.size === 0 || file.name.length === 0) {
throw new Error("File is empty");
}
if (!model.reverse_model) {
try {
// if the file is an image, it will be parsed as an image by local parser first
const couldLocalVision = model.vision_model;
if (couldLocalVision && file.type.startsWith("image/")) {
console.log("[parser] hit image/* file, using local parser");
// parse image as base64 (e.g. result: data:image/png;base64,xxx)
const base64 = await fileToBase64(file);
return base64;
}
// if the file is txt, parse it as txt
if (
file.type === "text/plain" ||
checkFileSuffix(file.name, [
"txt",
"md",
"markdown",
"json",
"xml",
"csv",
"yaml",
"yml",
"toml",
"ini",
"cfg",
"conf",
])
) {
console.log("[parser] hit text/plain file, using local parser");
return await file.text();
}
console.log(file.type);
} catch (e) {
console.error(
"[parser] local parser failed, switch to server parser: ",
e,
);
}
}
return blobParser(file, model, onProgress);
}
export async function blobParser(
file: File,
model: Model,
onProgress?: (progress: number) => void,
): Promise {
const endpoint = trimSuffixes(blobEndpoint, ["/upload", "/"]);
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append("file", file);
formData.append("model", model.id);
formData.append("enable_ocr", (model.ocr_model ?? false).toString());
formData.append("enable_vision", (model.vision_model ?? false).toString());
formData.append("save_all", (model.reverse_model ?? false).toString());
xhr.open("POST", `${endpoint}/upload`, true);
xhr.upload.onprogress = (progressEvent) => {
console.debug(progressEvent);
if (progressEvent.lengthComputable) {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total,
);
console.debug(percentCompleted);
onProgress?.(percentCompleted);
}
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const data = JSON.parse(xhr.responseText) as BlobParserResponse;
if (!data.status) {
reject(new Error(data.error));
} else if (data.content.length === 0) {
reject(new Error("Result is empty"));
} else {
resolve(data.content);
}
} catch (e) {
reject(new Error("Invalid JSON response"));
}
} else {
reject(new Error(`HTTP error! status: ${xhr.status}`));
}
};
xhr.onerror = () => {
reject(new Error("Network error"));
};
xhr.send(formData);
});
}
================================================
FILE: app/src/api/generation.ts
================================================
import { tokenField, websocketEndpoint } from "@/conf/bootstrap.ts";
import { getMemory } from "@/utils/memory.ts";
export const endpoint = `${websocketEndpoint}/generation/create`;
export type GenerationForm = {
token: string;
prompt: string;
model: string;
};
export type GenerationSegmentResponse = {
message: string;
quota: number;
end: boolean;
error: string;
hash: string;
title?: string;
};
export type MessageEvent = {
message: string;
quota: number;
};
export class GenerationManager {
protected processing: boolean;
protected connection: WebSocket | null;
protected message: string;
protected onProcessingChange?: (processing: boolean) => void;
protected onMessage?: (message: MessageEvent) => void;
protected onError?: (error: string) => void;
protected onFinished?: (hash: string) => void;
constructor() {
this.processing = false;
this.connection = null;
this.message = "";
}
public setProcessingChangeHandler(
handler: (processing: boolean) => void,
): void {
this.onProcessingChange = handler;
}
public setMessageHandler(handler: (message: MessageEvent) => void): void {
this.onMessage = handler;
}
public setErrorHandler(handler: (error: string) => void): void {
this.onError = handler;
}
public setFinishedHandler(handler: (hash: string) => void): void {
this.onFinished = handler;
}
public isProcessing(): boolean {
return this.processing;
}
protected setProcessing(processing: boolean): boolean {
this.processing = processing;
if (!processing) {
this.connection = null;
this.message = "";
}
this.onProcessingChange?.(processing);
return processing;
}
public getConnection(): WebSocket | null {
return this.connection;
}
protected handleMessage(message: GenerationSegmentResponse): void {
if (message.error && message.end) {
this.onError?.(message.error);
this.setProcessing(false);
return;
}
this.message += message.message;
this.onMessage?.({
message: this.message,
quota: message.quota,
});
if (message.end) {
this.onFinished?.(message.hash);
this.setProcessing(false);
}
}
public generate(prompt: string, model: string) {
this.setProcessing(true);
const token = getMemory(tokenField) || "anonymous";
if (token) {
this.connection = new WebSocket(endpoint);
this.connection.onopen = () => {
this.connection?.send(
JSON.stringify({ token, prompt, model } as GenerationForm),
);
};
this.connection.onmessage = (event) => {
this.handleMessage(JSON.parse(event.data) as GenerationSegmentResponse);
};
this.connection.onclose = () => {
this.setProcessing(false);
};
}
}
public generateWithBlock(prompt: string, model: string): boolean {
if (this.isProcessing()) {
return false;
}
this.generate(prompt, model);
return true;
}
}
export const manager = new GenerationManager();
================================================
FILE: app/src/api/history.ts
================================================
import axios from "axios";
import type { ConversationInstance } from "./types.tsx";
import { setHistory } from "@/store/chat.ts";
import { AppDispatch } from "@/store";
import { CommonResponse } from "@/api/common.ts";
import { getErrorMessage } from "@/utils/base.ts";
import { VirtualWebSearchRole, VirtualRolePrefix, Message } from "./types.tsx";
import { formatToolCallResult } from "@/api/plugin.ts";
export async function getConversationList(): Promise {
try {
const resp = await axios.get("/conversation/list");
return (
resp.data.status ? resp.data.data || [] : []
) as ConversationInstance[];
} catch (e) {
console.warn(e);
return [];
}
}
export async function updateConversationList(
dispatch: AppDispatch,
): Promise {
const resp = await getConversationList();
dispatch(setHistory(resp));
}
export async function loadConversation(
id: number,
): Promise {
try {
const resp = await axios.get(`/conversation/load?id=${id}`);
if (resp.data.status) {
const conversation = resp.data.data as ConversationInstance;
if (conversation.message && conversation.message.length > 0) {
const processedMessages: Message[] = [];
for (let i = 0; i < conversation.message.length; i++) {
const currentMsg = conversation.message[i];
if (currentMsg.role === VirtualWebSearchRole) {
let nextMsgIndex = i + 1;
while (
nextMsgIndex < conversation.message.length &&
conversation.message[nextMsgIndex].role.startsWith(VirtualRolePrefix)
) {
nextMsgIndex++;
}
if (nextMsgIndex < conversation.message.length) {
conversation.message[nextMsgIndex].search_query = currentMsg.search_query;
conversation.message[nextMsgIndex].search_result = currentMsg.search_result;
conversation.message[nextMsgIndex].search_index = currentMsg.search_index;
}
continue;
}
if (currentMsg.role === "assistant" && currentMsg.tool_calls) {
processedMessages.push(currentMsg);
} else if (currentMsg.role === "tool" && currentMsg.tool_call_id) {
const toolCallId = currentMsg.tool_call_id;
for (let j = processedMessages.length - 1; j >= 0; j--) {
const prevMsg = processedMessages[j];
if (prevMsg.role === "assistant" && prevMsg.tool_calls) {
const toolCall = prevMsg.tool_calls.find(tc => tc.id === toolCallId);
if (toolCall) {
try {
const result = JSON.parse(currentMsg.content);
if (result.error) {
toolCall.error = result.error;
toolCall.status = "error";
} else {
const formattedResult = formatToolCallResult(currentMsg.content);
toolCall.result = formattedResult;
toolCall.status = "success";
}
} catch {
const formattedResult = formatToolCallResult(currentMsg.content);
toolCall.result = formattedResult;
toolCall.status = "success";
}
}
break;
}
}
processedMessages.push(currentMsg);
} else {
processedMessages.push(currentMsg);
}
}
conversation.message = processedMessages;
}
return conversation;
}
return { id, name: "", message: [] };
} catch (e) {
console.warn(e);
return { id, name: "", message: [] };
}
}
export async function deleteConversation(id: number): Promise {
try {
const resp = await axios.get(`/conversation/delete?id=${id}`);
return resp.data.status;
} catch (e) {
console.warn(e);
return false;
}
}
export async function renameConversation(
id: number,
name: string,
): Promise {
try {
const resp = await axios.post("/conversation/rename", { id, name });
return resp.data as CommonResponse;
} catch (e) {
console.warn(e);
return { status: false, error: getErrorMessage(e) };
}
}
export async function deleteAllConversations(): Promise {
try {
const resp = await axios.get("/conversation/clean");
return resp.data.status;
} catch (e) {
console.warn(e);
return false;
}
}
================================================
FILE: app/src/api/mask.ts
================================================
import { CustomMask } from "@/masks/types.ts";
import axios from "axios";
import { CommonResponse } from "@/api/common.ts";
import { getErrorMessage } from "@/utils/base.ts";
type ListMaskResponse = CommonResponse & {
data: CustomMask[];
};
export async function listMasks(): Promise {
try {
const resp = await axios.get("/conversation/mask/view");
return (
resp.data ?? {
status: true,
data: [],
}
);
} catch (e) {
return {
status: false,
data: [],
error: getErrorMessage(e),
};
}
}
export async function saveMask(mask: CustomMask): Promise {
try {
const resp = await axios.post("/conversation/mask/save", mask);
return resp.data;
} catch (e) {
return {
status: false,
error: getErrorMessage(e),
};
}
}
export async function deleteMask(id: number): Promise {
try {
const resp = await axios.post("/conversation/mask/delete", { id });
return resp.data;
} catch (e) {
return {
status: false,
error: getErrorMessage(e),
};
}
}
================================================
FILE: app/src/api/plugin.ts
================================================
import axios from "axios";
import { CommonResponse } from "@/api/common.ts";
import { getErrorMessage } from "@/utils/base.ts";
type ListPluginResponse = CommonResponse & {
data: Plugin[];
};
type TestPluginResponse = CommonResponse & {
data?: {
tools?: Array<{
name: string;
description: string;
inputSchema: Record;
}>;
};
};
export async function listPlugins(): Promise {
try {
const resp = await axios.get("/conversation/plugin/view");
return (
resp.data ?? {
status: true,
data: [],
}
);
} catch (e) {
return {
status: false,
data: [],
error: getErrorMessage(e),
};
}
}
export async function savePlugin(plugin: Partial): Promise {
try {
const resp = await axios.post("/conversation/plugin/save", plugin);
return resp.data;
} catch (e) {
return {
status: false,
error: getErrorMessage(e),
};
}
}
export async function deletePlugin(id: number): Promise {
try {
const resp = await axios.post("/conversation/plugin/delete", { id });
return resp.data;
} catch (e) {
return {
status: false,
error: getErrorMessage(e),
};
}
}
export async function testPlugin(serverUrl: string): Promise {
try {
const resp = await axios.get("/conversation/plugin/test", {
params: { server_url: serverUrl }
});
return resp.data;
} catch (e) {
return {
status: false,
error: getErrorMessage(e),
};
}
}
export function formatToolCallResult(result: string): string {
try {
const parsed = JSON.parse(result);
let textContent = '';
if (parsed.text) {
textContent = parsed.text;
} else if (typeof parsed === 'string') {
textContent = parsed;
} else {
textContent = result;
}
try {
const secondParsed = JSON.parse(textContent);
if (secondParsed.text) {
return secondParsed.text;
} else if (typeof secondParsed === 'string') {
return secondParsed;
} else {
return JSON.stringify(secondParsed, null, 2);
}
} catch {
return textContent;
}
} catch {
return result;
}
}
export function parseMCPInput(input: string): {
status: 'success' | 'error' | 'noop';
identifier?: string;
mcpConfig?: {
name: string;
description: string;
server_url: string;
};
errorCode?: string;
} {
try {
const parsed = JSON.parse(input);
if (!parsed.mcpServers || typeof parsed.mcpServers !== 'object') {
return { status: 'error', errorCode: 'plugin.import-error.invalid-format' };
}
const serverKeys = Object.keys(parsed.mcpServers);
if (serverKeys.length === 0) {
return { status: 'error', errorCode: 'plugin.import-error.no-servers' };
}
const identifier = serverKeys[0];
const serverConfig = parsed.mcpServers[identifier];
let server_url = '';
let description = '';
if (serverConfig.url) {
server_url = serverConfig.url;
description = `HTTP MCP Server: ${server_url}`;
} else if (serverConfig.command) {
return { status: 'error', errorCode: 'plugin.import-error.stdio-not-supported' };
} else {
return { status: 'error', errorCode: 'plugin.import-error.unsupported-config' };
}
return {
status: 'success',
identifier,
mcpConfig: {
name: identifier,
description,
server_url,
}
};
} catch (error) {
return { status: 'error', errorCode: 'plugin.import-error.invalid-json' };
}
}
================================================
FILE: app/src/api/quota.ts
================================================
import axios from "axios";
export async function getQuota(): Promise {
try {
const response = await axios.get("/quota");
if (response.data.status) {
return response.data.quota as number;
}
} catch (e) {
console.debug(e);
}
return NaN;
}
================================================
FILE: app/src/api/record.ts
================================================
import { CommonResponse } from "@/api/common.ts";
import axios from "axios";
export type Record = {
username: string;
type: string;
token_name: string;
model: string;
input_tokens: number;
output_tokens: number;
quota: number;
duration: number;
detail: string;
prompts: string;
response_prompts: string;
channel?: number;
channel_name?: string;
created_at: string;
};
export type RecordData = {
total: number;
records: Record[];
};
export type RecordStats = {
billing_today: number;
billing_month: number;
request_today: number;
request_month: number;
rpm: number;
tpm: number;
};
export type RecordQuery = {
user_id?: number;
start_time?: string;
end_time?: string;
token_name?: string;
model?: string;
type?: RecordType;
show_channel?: boolean;
};
type ListRecordsResponse = CommonResponse & {
data?: RecordData;
};
type RecordStatsResponse = CommonResponse & {
data?: RecordStats;
};
export enum RecordType {
All = "all",
Topup = "topup",
Consume = "consume",
System = "system",
}
export const RecordTypes = [
RecordType.All,
RecordType.Topup,
RecordType.Consume,
RecordType.System,
];
export async function listRecords(
page: number,
options?: RecordQuery,
): Promise {
try {
const payload: Partial = { ...options };
if (options && options.show_channel === undefined) {
delete payload.show_channel;
}
const resp = await axios.post(`/record/view?page=${page}`, payload);
return resp.data as ListRecordsResponse;
} catch (e) {
return {
status: false,
message: (e as Error).message,
};
}
}
export async function getRecordStats(): Promise {
try {
const resp = await axios.post(`/record/stats`);
return resp.data as RecordStatsResponse;
} catch (e) {
return {
status: false,
message: (e as Error).message,
};
}
}
================================================
FILE: app/src/api/redeem.ts
================================================
import axios from "axios";
import { getErrorMessage } from "@/utils/base.ts";
export type RedeemResponse = {
status: boolean;
error: string;
quota: number;
};
export async function useRedeem(code: string): Promise {
try {
const resp = await axios.get(`/redeem?code=${code}`);
return resp.data as RedeemResponse;
} catch (e) {
console.debug(e);
return {
status: false,
error: `network error: ${getErrorMessage(e)}`,
quota: 0,
};
}
}
================================================
FILE: app/src/api/sharing.ts
================================================
import axios from "axios";
import { Message } from "./types.tsx";
export type SharingForm = {
status: boolean;
message: string;
data: string;
};
export type SharingPreviewForm = {
name: string;
conversation_id: number;
hash: string;
time: string;
};
export type ViewData = {
name: string;
username: string;
time: string;
model?: string;
messages: Message[];
};
export type ViewForm = {
status: boolean;
message: string;
data: ViewData | null;
};
export type ListSharingResponse = {
status: boolean;
message: string;
data?: SharingPreviewForm[];
};
export type DeleteSharingResponse = {
status: boolean;
message: string;
};
export async function shareConversation(
id: number,
refs: number[] = [-1],
): Promise {
try {
const resp = await axios.post("/conversation/share", { id, refs });
return resp.data;
} catch (e) {
return { status: false, message: (e as Error).message, data: "" };
}
}
export async function viewConversation(hash: string): Promise {
try {
const resp = await axios.get(`/conversation/view?hash=${hash}`);
return resp.data as ViewForm;
} catch (e) {
return {
status: false,
message: (e as Error).message,
data: null,
};
}
}
export async function listSharing(): Promise {
try {
const resp = await axios.get("/conversation/share/list");
return resp.data as ListSharingResponse;
} catch (e) {
return {
status: false,
message: (e as Error).message,
};
}
}
export async function deleteSharing(
hash: string,
): Promise {
try {
const resp = await axios.get(`/conversation/share/delete?hash=${hash}`);
return resp.data as DeleteSharingResponse;
} catch (e) {
return {
status: false,
message: (e as Error).message,
};
}
}
export function getSharedLink(hash: string): string {
return `${location.origin}/share/${hash}`;
}
================================================
FILE: app/src/api/types.tsx
================================================
import { ChargeBaseProps } from "@/admin/charge.ts";
import { useMemo } from "react";
import { BotIcon, ServerIcon, UserIcon } from "lucide-react";
export const UserRole = "user";
export const AssistantRole = "assistant";
export const SystemRole = "system";
export const VirtualRolePrefix = "virtualRole::";
export const VirtualWebSearchRole = "virtualRole::websearch";
export type Role = typeof UserRole | typeof AssistantRole | typeof SystemRole;
export const Roles = [UserRole, AssistantRole, SystemRole];
export const getRoleIcon = (role: string) => {
return useMemo(() => {
switch (role) {
case UserRole:
return ;
case AssistantRole:
return ;
case SystemRole:
return ;
default:
return ;
}
}, [role]);
};
export type Message = {
role: string;
content: string;
keyword?: string;
quota?: number;
end?: boolean;
plan?: boolean;
search_query?: {
type: string;
search_queries: string[];
};
search_result?: {
type: string;
search_results: Array<{
url: string;
title: string;
snippet: string;
published_at?: number;
site_name?: string;
site_icon?: string;
}>;
};
search_index?: {
type: string;
search_indexes: Array<{
url: string;
cite_index: number;
}>;
};
tool_calls?: Array<{
index: number;
type: string;
id: string;
function: {
name: string;
arguments: string;
};
status?: "start" | "executing" | "success" | "error";
result?: string;
error?: string;
}>;
tool_call_id?: string;
name?: string;
response_type?: string;
};
export type Model = {
id: string;
name: string;
description?: string;
free: boolean;
auth: boolean;
default: boolean;
high_context: boolean;
function_calling?: boolean;
vision_model?: boolean;
ocr_model?: boolean;
reverse_model?: boolean;
thinking_model?: boolean;
avatar: string;
tag?: string[];
price?: ChargeBaseProps;
};
export type Id = number;
export type ConversationInstance = {
id: number;
name: string;
message: Message[];
model?: string;
shared?: boolean;
};
export type PlanItem = {
id: string;
name: string;
value: number;
icon: string;
models: string[];
};
export type Plan = {
level: number;
price: number;
items: PlanItem[];
discounts?: Record;
};
export type Plans = Plan[];
export function newModel(id: string, name?: string, avatar?: string): Model {
return {
id,
name: name ?? id,
avatar: avatar ?? "",
free: false,
auth: false,
default: false,
high_context: false,
};
}
================================================
FILE: app/src/api/v1.ts
================================================
import axios from "axios";
import { Model, Plan } from "@/api/types.tsx";
import { ChargeProps, nonBilling } from "@/admin/charge.ts";
import { getErrorMessage } from "@/utils/base.ts";
type v1Options = {
endpoint?: string;
};
type v1Models = {
object: string;
data: v1ModelItem[];
};
type v1ModelItem =
| string
| {
id: string;
object: string;
created: number;
owned_by: string;
};
type v1Resp = {
data: T;
status: boolean;
error?: string;
};
export type v1ApiKey = {
id: number;
user_id?: number;
name: string;
expired_at: string;
quota: number;
used_quota: number;
infinite_quota: boolean;
ip_whitelist: string;
model_whitelist: string;
token_group?: string;
group_id?: string;
api_key?: string;
disabled: boolean;
created_at?: string;
};
export const initialApiKey: v1ApiKey = {
id: -1,
name: "",
expired_at: "1970-01-01 00:00:00",
quota: 100.0,
used_quota: 0,
infinite_quota: false,
ip_whitelist: "",
model_whitelist: "",
token_group: "default",
disabled: false,
};
export function getModelName(id: string): string {
// replace all `-` to ` ` except first `-` keep it
let begin = true;
return id
.replace(/-/g, (l) => {
if (begin) {
begin = false;
return l;
}
return " ";
})
.replace(/\b\w/g, (l) => l.toUpperCase())
.replace(/Gpt/g, "GPT")
.replace(/Tts/g, "TTS")
.replace(/Dall-E/g, "DALL-E")
.replace(/Dalle/g, "DALLE")
.replace(/Glm/g, "GLM")
.trim();
}
export function getV1Path(path: string, options?: v1Options): string {
let endpoint = options && options.endpoint ? options.endpoint : "";
if (endpoint.endsWith("/")) endpoint = endpoint.slice(0, -1);
return endpoint + path;
}
export async function getApiModels(
secret?: string,
options?: v1Options,
): Promise> {
try {
const res = await axios.get(
getV1Path("/v1/models", options),
secret
? {
headers: {
Authorization: `Bearer ${secret}`,
},
}
: undefined,
);
const data = res.data as v1Models;
// if data.data is an array of strings, we can just return it
const models = data.data
? data.data.map((model) => (typeof model === "string" ? model : model.id))
: [];
return models.length > 0
? { status: true, data: models }
: { status: false, data: [], error: "No models found" };
} catch (e) {
console.warn(e);
return { status: false, data: [], error: getErrorMessage(e) };
}
}
export async function getApiPlans(options?: v1Options): Promise {
try {
const res = await axios.get(getV1Path("/v1/plans", options));
const plans = res.data as Plan[];
return plans.filter((plan: Plan) => plan.level !== 0);
} catch (e) {
console.warn(e);
return [];
}
}
export async function getApiMarket(options?: v1Options): Promise {
try {
const res = await axios.get(getV1Path("/v1/market", options));
return (res.data || []) as Model[];
} catch (e) {
console.warn(e);
return [];
}
}
export async function getFilledApiMarket(
secret?: string,
options?: v1Options,
): Promise {
const data = await getApiMarket(options);
if (data.length > 0) return data;
const resp = await getApiModels(secret, options);
if (!resp.status) return [];
return resp.data.map((id) => ({
id,
default: true,
name: getModelName(id),
tag: [],
avatar: "",
description: id,
free: false,
auth: true,
high_context: false,
price: {
type: nonBilling,
anonymous: false,
models: [id],
input: 0,
output: 0,
},
}));
}
export async function bindMarket(options?: v1Options): Promise {
const market = await getFilledApiMarket(undefined, options);
const charge = await getApiCharge(options);
market.forEach((item: Model) => {
const instance = charge.find((i: ChargeProps) =>
i.models.includes(item.id),
);
if (!instance) return;
item.free = instance.type === nonBilling;
item.auth = !item.free || !instance.anonymous;
item.price = { ...instance };
});
return market;
}
export async function getApiCharge(
options?: v1Options,
): Promise {
try {
const res = await axios.get(getV1Path("/v1/charge", options));
return res.data as ChargeProps[];
} catch (e) {
console.warn(e);
return [];
}
}
export async function listApiKey(
options?: v1Options,
): Promise> {
try {
const res = await axios.get(getV1Path("/v1/list_keys", options));
return res.data as v1Resp;
} catch (e) {
console.warn(e);
return { status: false, data: [], error: getErrorMessage(e) };
}
}
export async function updateApiKey(
data: v1ApiKey,
options?: v1Options,
): Promise> {
try {
const res = await axios.post(getV1Path("/v1/update_key", options), data);
return res.data as v1Resp;
} catch (e) {
console.warn(e);
return { status: false, data: initialApiKey, error: getErrorMessage(e) };
}
}
export async function deleteApiKey(
id: number,
options?: v1Options,
): Promise> {
try {
const res = await axios.post(getV1Path(`/v1/delete_key?id=${id}`, options));
return res.data as v1Resp;
} catch (e) {
console.warn(e);
return { status: false, data: initialApiKey, error: getErrorMessage(e) };
}
}
type ManifestJson = {
data?: Record<
string,
{
file: string;
src: string;
}
>;
status: boolean;
error?: string;
};
export async function getManifestJson(): Promise {
try {
const res = await axios.get("/manifest.json", {
baseURL: "/",
});
return {
status: true,
data: res.data,
};
} catch (e) {
console.warn(e);
return {
status: false,
error: getErrorMessage(e),
};
}
}
================================================
FILE: app/src/assets/admin/all.less
================================================
@import "menu";
@import "dashboard";
@import "market";
@import "management";
@import "broadcast";
@import "channel";
@import "charge";
@import "system";
@import "subscription";
@import "logger";
.admin-page {
position: relative;
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
max-width: 100%;
}
@media (orientation: landscape) {
.admin-page {
max-width: calc(100vw - 3.5rem);
}
}
.admin-container {
width: 100%;
height: max-content;
padding: 2rem;
display: flex;
flex-direction: column;
}
.admin-card {
position: relative;
border: 0 !important;
width: 100%;
height: 100%;
min-height: 20vh;
}
.record-card {
.record-wrapper > * {
width: 100%;
}
}
@media (max-width: 1268px) {
.admin-card {
border-radius: 0 !important;
}
.user-interface,
.market,
.broadcast,
.channel,
.charge,
.system,
.logger,
.admin-subscription,
.admin-container
{
padding: 0 !important;
& > * {
margin-bottom: 0 !important;
border-bottom: 1px solid hsl(var(--border)) !important;
border-radius: 0 !important;
}
}
}
.object-id {
display: flex;
flex-direction: row;
align-items: center;
justify-items: center;
border-radius: var(--radius);
border: 1px solid hsl(var(--border));
color: hsl(var(--text-secondary));
user-select: none;
font-size: 0.75rem;
height: 2.5rem;
padding: 0.5rem 1.25rem;
cursor: pointer;
transition: 0.25s;
flex-shrink: 0;
&:hover {
color: hsl(var(--text));
border-color: hsl(var(--border-hover));
}
svg {
transform: translateY(1px);
}
}
================================================
FILE: app/src/assets/admin/broadcast.less
================================================
.broadcast {
width: 100%;
height: max-content;
padding: 2rem;
display: flex;
flex-direction: column;
.broadcast-card {
width: 100%;
height: 100%;
min-height: 20vh;
}
.empty {
color: hsl(var(--text-secondary)) !important;
font-size: 14px;
margin: auto;
user-select: none;
}
}
================================================
FILE: app/src/assets/admin/channel.less
================================================
.channel {
width: 100%;
height: max-content;
padding: 2rem;
display: flex;
flex-direction: column;
.channel-card {
width: 100%;
height: 100%;
min-height: 20vh;
}
.channel-table {
.channel-id {
color: hsl(var(--text-secondary));
}
}
}
.channel-editor {
position: relative;
.channel-loader {
position: absolute;
top: 0;
right: 0.25rem;
}
}
.channel-wrapper {
display: flex;
flex-direction: column;
margin-top: 0.5rem;
margin-bottom: 2rem;
& > * {
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
}
.channel-row {
display: flex;
flex-direction: column;
user-select: none;
white-space: nowrap;
&.column-layout {
flex-direction: row;
align-items: center;
white-space: nowrap;
width: 100%;
.channel-content {
margin-left: 0;
margin-bottom: 0;
}
& > * {
margin-right: 1rem;
margin-bottom: 0;
&:last-child {
margin-right: 0;
}
}
}
.channel-content {
display: flex;
flex-direction: row;
align-items: center;
margin-left: 0.25rem;
margin-bottom: 0.5rem;
}
}
}
.channel-model-wrapper {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 0.5rem;
height: max-content;
width: 100%;
border-radius: var(--radius);
border: 1px solid hsl(var(--border));
background: hsl(var(--background));
padding: 1rem;
min-height: 5rem;
.channel-model-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 0.25rem 0.5rem;
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
transition: .25s;
height: max-content;
white-space: break-spaces;
&:hover {
border-color: hsl(var(--border-hover));
}
.remove-action {
width: 0.75rem;
height: 0.75rem;
cursor: pointer;
margin-left: 0.5rem;
color: hsl(var(--text-secondary));
transition: .25s;
flex-shrink: 0;
&:hover {
color: hsl(var(--text-primary));
}
}
}
}
.channel-model-action {
display: flex;
flex-direction: row;
width: 100%;
flex-wrap: wrap;
gap: 0.5rem;
@media (max-width: 620px) {
& > * {
width: 100%;
}
}
}
.channel-description {
white-space: break-spaces;
line-height: 1.25em;
}
================================================
FILE: app/src/assets/admin/charge.less
================================================
.charge {
width: 100%;
height: max-content;
padding: 2rem;
display: flex;
flex-direction: column;
.charge-card {
width: 100%;
height: 100%;
min-height: 20vh;
}
}
.charge-widget {
height: max-content;
width: 100%;
display: flex;
flex-direction: column;
& > * {
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
}
}
.charge-alert {
.model-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
.model {
padding: 0.5rem 0.75rem;
border-radius: var(--radius);
border: 1px solid hsl(var(--border));
}
}
}
.charge-editor {
padding: 1.5rem;
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
.token {
color: hsl(var(--text-secondary));
user-select: none;
}
}
.charge-table {
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
overflow-x: auto;
scrollbar-width: thin;
&::-webkit-scrollbar {
width: 0.5rem;
}
.table {
scrollbar-width: thin;
}
.charge-id {
color: hsl(var(--text-secondary));
user-select: none;
&:before {
content: '#';
}
}
}
================================================
FILE: app/src/assets/admin/dashboard.less
================================================
.dashboard {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding: 0.5rem 0;
& > * {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
}
}
.info-boxes {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1rem;
width: 100%;
height: max-content;
padding: 1rem 2rem 0;
@media (max-width: 940px) {
grid-template-columns: 1fr;
padding: 1rem 1.5rem 0;
}
.info-box {
display: flex;
flex-direction: row;
flex-grow: 1;
height: max-content;
padding: 0.75rem 1.5rem;
border-radius: var(--radius);
border: 1px solid hsl(var(--border));
box-shadow: 0.5rem 0.5rem 1rem 0 hsl(var(--shadow));
user-select: none;
width: 100%;
margin-right: 1.5rem;
margin-left: auto;
&:last-child {
margin-right: auto;
}
& > * {
flex-shrink: 0;
}
.box-wrapper {
flex-grow: 1;
.box-title {
font-size: 1rem;
margin-bottom: 0.5rem;
}
.box-value {
font-size: 1.5rem;
font-weight: normal;
&.money::after,
.box-subvalue {
font-size: 1rem;
font-weight: normal;
margin-left: 0.5rem;
content: 'CNY';
}
}
}
.box-icon {
width: max-content;
height: max-content;
transform: translate(0.25rem, 0.25rem);
border-radius: 0.25rem;
svg {
width: 2rem;
height: 2rem;
stroke-width: 1.25;
}
}
}
}
.chart-boxes {
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding: 0 1.5rem 1rem;
width: 100%;
@media (max-width: 940px) {
flex-direction: column;
padding: 0 1rem 1rem;
.chart-box {
width: calc(100% - 1rem) !important;
}
}
.chart-box {
width: calc(50% - 1rem);
height: max-content;
min-height: 235px;
padding: 1rem 2rem;
margin: 0.5rem;
border-radius: var(--radius);
background: hsl(var(--background));
box-shadow: 0.5rem 0.5rem 1rem 0 var(--shadow);
user-select: none;
border: 1px solid hsl(var(--border));
.chart {
#model-usage-chart,
#user-type-chart {
min-height: 8rem !important;
max-height: 8rem !important;
margin-top: 1.5rem;
margin-bottom: 1rem;
flex-shrink: 0;
}
.common-chart {
min-height: 10rem !important;
max-height: 10rem !important;
flex-shrink: 0;
font-size: 0.8rem !important;
font-family: var(--font-family) !important;
}
.chart-title {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
.chart-title-info {
color: hsl(var(--text-secondary));
}
svg {
position: relative;
top: 1px;
}
& > * {
flex-shrink: 0;
margin-right: 0.25rem;
&:last-child {
margin-right: 0;
}
}
}
}
}
}
================================================
FILE: app/src/assets/admin/logger.less
================================================
.logger {
width: 100%;
height: max-content;
padding: 2rem;
display: flex;
flex-direction: column;
.logger-card {
width: 100%;
height: 100%;
min-height: 20vh;
}
}
.logger-container {
.paragraph-header {
margin-bottom: 0.5rem;
}
}
.logger-toolbar {
display: flex;
flex-direction: row;
align-items: center;
input {
max-width: 4.5rem;
text-align: center;
}
button {
flex-shrink: 0;
}
& > * {
margin-right: 0.5rem !important;
white-space: nowrap;
&:last-child {
margin-right: 0;
}
}
}
.logger-console {
position: relative;
border-radius: var(--radius);
font-size: 14px;
width: 100%;
height: max-content;
overflow: hidden;
pre {
width: 100%;
height: max-content;
min-height: 20vh;
max-height: 60vh;
overflow-x: hidden;
overflow-y: auto;
touch-action: pan-y;
padding: 0.5rem;
white-space: pre-wrap !important;
}
.console-icon {
position: absolute;
top: 0.75rem;
right: 0.75rem;
user-select: none;
}
}
.logger-list {
display: flex;
flex-direction: column;
width: 100%;
height: max-content;
& > * {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
}
}
.logger-item {
display: flex;
flex-direction: row;
padding: 0.75rem 1rem;
flex-wrap: wrap;
border-radius: var(--radius);
border: 1px solid hsl(var(--border));
transition: all 0.2s ease-in-out;
align-items: center;
&:hover {
border-color: hsl(var(--border-hover));
}
& > * {
margin-right: 1rem;
flex-shrink: 0;
white-space: nowrap;
&:last-child {
margin-right: 0;
}
}
.logger-item-title {
font-size: 16px;
color: hsl(var(--text));
}
.logger-item-size {
font-size: 14px;
color: hsl(var(--text-secondary));
}
.logger-item-action {
cursor: pointer;
}
}
================================================
FILE: app/src/assets/admin/management.less
================================================
.user-interface {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
max-width: 100%;
height: 100%;
padding: 2rem;
& > * {
margin-bottom: 2rem;
&:last-child {
margin-bottom: 0;
}
}
&.mobile {
padding: 1rem;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
margin-top: 1rem;
& > * {
scale: 0.8;
margin: 0 0.5rem;
}
}
.empty {
user-select: none;
text-align: center;
font-size: 14px;
margin: 4rem 0 2rem;
color: hsl(var(--text-secondary));
}
.action {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 1rem;
}
}
.user-row,
.redeem-row,
.invitation-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
white-space: nowrap;
user-select: none;
color: hsl(var(--text));
margin: 1rem 0;
}
.user-action,
.redeem-action,
.invitation-action {
display: flex;
margin-top: 1rem;
flex-direction: row;
align-items: center;
& > * {
margin-right: 0.5rem;
&:last-child {
margin-right: 0;
}
}
}
================================================
FILE: app/src/assets/admin/market.less
================================================
.market {
width: 100%;
height: max-content;
padding: 2rem;
display: flex;
flex-direction: column;
.market-card {
width: 100%;
height: 100%;
min-height: 20vh;
}
& > * {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
}
.model-combobox {
width: 320px;
margin-left: auto;
@media (max-width: 768px) {
width: 100% !important;
margin-left: 0 !important;
max-width: 100vw !important;
}
}
}
.market-alert {
display: flex;
flex-direction: column;
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
margin-bottom: 1rem;
padding: 0.75rem;
.market-alert-wrapper {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 0.5rem;
}
}
.market-list {
display: flex;
flex-direction: column;
& > * {
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
}
.empty {
display: flex;
text-align: center;
color: hsl(var(--text-secondary));
user-select: none;
margin: 1.5rem auto;
}
.market-item {
display: flex;
flex-direction: row;
padding: 1rem 1rem 1.5rem;
border-radius: var(--radius);
border: 1px solid hsl(var(--border));
user-select: none;
width: 100%;
height: max-content;
align-items: center;
background: hsl(var(--card));
transition: 0.25s;
transition-property: border, background;
&.error {
border-color: hsl(var(--error));
}
&.stacked {
border-color: transparent !important;
padding: 0.25rem 0.5rem;
margin-bottom: 0.25rem;
.market-row {
width: max-content;
flex-wrap: nowrap;
flex-shrink: 0;
gap: 0.5rem;
}
}
.market-tags {
display: flex;
flex-direction: row;
gap: 0.5rem;
flex-wrap: wrap;
width: 100%;
.market-tag {
white-space: nowrap;
padding: 0.25rem 0.75rem !important;
}
}
.market-image-wrapper {
display: flex;
flex-direction: column;
width: 100%;
height: max-content;
& > * {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
}
}
.market-custom-image {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 0.25rem;
.market-checkbox {
display: flex;
flex-direction: row;
align-items: center;
margin: 0.25rem 0;
button {
margin-right: 0.25rem;
transform: translateY(1px);
}
}
}
.market-images {
display: flex;
flex-direction: row;
gap: 0.5rem;
flex-wrap: wrap;
width: 100%;
.market-image {
width: 2.5rem;
height: 2.5rem;
padding: 0.25rem;
transition: 0.1s;
img {
width: 2rem;
height: 2rem;
opacity: 0.6;
border-radius: calc(var(--radius) - 2px);
transition: 0.1s;
}
&.active {
img {
opacity: 1;
}
}
}
}
svg {
flex-shrink: 0;
}
.drop-icon {
color: hsl(var(--text-secondary));
transition: color 0.25s ease;
}
&:hover {
.drop-icon {
color: hsl(var(--text));
}
}
.model-wrapper {
display: flex;
flex-direction: column;
flex-grow: 1;
flex-basis: 0;
& > * {
margin-bottom: 0.75rem;
&:last-child {
margin-bottom: 0;
}
}
}
.market-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
width: 100%;
gap: 0.75rem;
& > span {
display: flex;
flex-direction: row;
align-items: center;
font-size: 0.9rem;
white-space: nowrap;
min-width: 68px;
text-align: center;
svg {
transform: translateY(1px);
}
}
}
}
}
================================================
FILE: app/src/assets/admin/menu.less
================================================
.admin-menu {
display: flex;
flex-shrink: 0;
flex-direction: column;
height: 100%;
padding: 0.75rem;
margin: 0;
background: hsl(var(--background));
transition: 0.225s ease-in-out;
min-height: calc(100% - var(--navbar-height));
transition-property: width, background, box-shadow, opacity;
border-right: 0;
overflow-x: hidden;
width: 4.25rem;
border-right: 1px solid hsl(var(--border));
opacity: 1;
pointer-events: all;
&.open {
width: 12rem;
.menu-item-title {
display: block !important;
opacity: 1 !important;
}
.menu-item-badge {
display: block !important;
opacity: 1 !important;
}
}
.menu-item {
display: flex;
flex-direction: row;
width: 100%;
height: fit-content;
padding: 0.5rem;
align-items: center;
user-select: none;
cursor: pointer;
transition: 0.2s ease-in-out;
border-radius: var(--radius);
font-size: 16px;
color: hsl(var(--text-secondary));
&:hover {
color: hsl(var(--text));
}
&.active {
color: hsl(var(--text));
background: hsl(var(--card-hover));
}
& > * {
flex-shrink: 0;
}
.menu-item-title {
font-size: 0.85rem;
margin-left: 0.25rem;
font-weight: normal;
display: none;
opacity: 0;
}
.menu-item-badge {
display: none;
opacity: 0;
}
.menu-item-icon {
width: fit-content;
height: fit-content;
padding: 0.25rem;
transform: translateY(1px);
svg {
width: 1.25rem;
height: 1.25rem;
stroke-width: 1.5;
}
}
}
@media (max-width: 668px) {
&.open {
width: 100%;
border-right: 0;
}
}
}
.admin-content {
flex-grow: 1;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
touch-action: pan-y;
background: hsla(var(--background-container));
& > .scrollarea-viewport > div {
display: flex !important;
}
}
================================================
FILE: app/src/assets/admin/subscription.less
================================================
.admin-subscription {
width: 100%;
height: max-content;
padding: 2rem;
display: flex;
flex-direction: column;
.sub-card {
width: 100%;
height: 100%;
min-height: 20vh;
}
}
.plan-config {
display: flex;
flex-direction: column;
margin-top: 0.25rem;
& > * {
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
}
.plan-config-row {
display: flex;
flex-direction: row;
align-items: center;
}
.plan-config-card {
display: flex;
flex-direction: column;
padding: 1rem;
border-radius: var(--radius);
border: 1px solid hsl(var(--border));
.plan-config-title {
display: flex;
flex-direction: row;
align-items: center;
white-space: nowrap;
user-select: none;
margin-bottom: 0.75rem;
&:before {
display: inline-block;
content: '';
margin-right: 0.5rem;
height: 1.25rem;
width: 2px;
border-radius: 1px;
background: hsl(var(--text-secondary));
transition: .25s;
}
}
.plan-items-action {
display: flex;
flex-direction: row;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
margin-top: 1rem;
}
.plan-items-wrapper {
display: flex;
flex-direction: column;
width: 100%;
height: max-content;
margin-top: 1rem;
.plan-item {
padding: 1rem;
border-radius: var(--radius);
border: 1px solid hsl(var(--border));
&.stacked {
flex-direction: row;
.plan-editor-row {
margin-bottom: 0;
flex-grow: 1;
margin-right: 1rem;
.plan-editor-label {
min-width: 0;
margin-right: 0.75rem;
flex-shrink: 0;
@media (max-width: 768px) {
svg {
display: none;
}
}
svg {
margin: 0 0.25rem;
}
}
}
}
.plan-editor-row > p {
min-width: 4.25rem;
}
}
& > * {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
}
}
.plan-editor-row {
display: flex;
flex-direction: row;
align-items: center;
.plan-editor-label {
display: flex;
flex-direction: row;
align-items: center;
white-space: nowrap;
margin-right: 0.5rem;
user-select: none;
svg {
display: inline-block;
flex-shrink: 0;
transform: translateY(-2px);
}
}
& > p {
white-space: nowrap;
}
}
& > * {
margin-bottom: 0.25rem;
&:last-child {
margin-bottom: 0;
}
}
}
}
================================================
FILE: app/src/assets/admin/system.less
================================================
.system {
width: 100%;
height: max-content;
padding: 2rem;
display: flex;
flex-direction: column;
.system-card {
width: 100%;
height: 100%;
min-height: 50vh;
}
}
================================================
FILE: app/src/assets/common/404.less
================================================
.error-page {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
font-size: 30px;
color: hsl(var(--tw-content));
gap: 12px;
width: 100%;
user-select: none;
.icon {
width: 58px;
height: 58px;
transform: translateY(-62px);
}
h1 {
font-size: 48px;
transform: translateY(-50px);
}
p {
font-size: 24px;
transform: translateY(-32px);
}
button {
transform: translateY(-4px);
}
}
================================================
FILE: app/src/assets/common/editor.less
================================================
.editor-action {
position: absolute;
top: 50%;
right: 8px;
transform: translateY(-50%);
background: hsl(var(--input)) !important;
padding: 6px;
border-radius: 50%;
cursor: pointer;
transition: 0.1s;
outline: 0;
opacity: 0;
&.active {
opacity: 1;
}
&:hover {
background: hsl(var(--border-hover)) !important;
}
}
.editor-dialog {
max-width: min(90vw, 920px) !important;
}
.editor-container {
padding: 2px 4px 0;
}
.editor-wrapper {
padding: 4px 0;
}
.editor-object {
position: relative;
display: grid;
grid-gap: 12px;
height: 100%;
&.show-editor {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
grid-template-areas: 'editor';
}
&.show-preview {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
grid-template-areas: 'markdown';
}
&.show-editor.show-preview {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
grid-template-areas: 'editor markdown';
}
}
.editor-input {
grid-area: editor;
scrollbar-width: thin;
height: max-content;
transition: .1s;
min-height: 50vh !important;
color: hsl(var(--text));
font-size: 16px !important;
padding: 14px 12px !important;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
}
.editor-preview {
height: 100%;
grid-area: markdown;
overflow: auto;
padding: 12px;
border-radius: 4px;
color: hsl(var(--text));
border: 1px solid hsl(var(--border));
scrollbar-width: thin;
transition: 0.1s;
min-height: 50vh !important;
font-size: 16px;
text-align: left;
}
.editor-toolbar {
display: flex;
width: 100%;
margin: 8px 0 4px;
& > * {
margin-right: 4px;
&:last-child {
margin-right: 0;
}
}
}
================================================
FILE: app/src/assets/common/file.less
================================================
.file-action {
position: absolute;
top: 50%;
left: 8px;
transform: translateY(-50%);
background: hsl(var(--input)) !important;
padding: 6px;
border-radius: 50%;
cursor: pointer;
transition: 0.1s;
outline: 0;
&:hover {
background: hsl(var(--border-hover)) !important;
}
}
.file-dialog {
max-width: min(90vw, 720px) !important;
}
.file-wrapper {
max-width: calc(90vw - 3rem);
}
.drop-window {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
min-height: 200px;
height: 20vh;
border: 2px dashed hsl(var(--border));
border-radius: var(--radius);
transition: 0.25s;
margin-top: 1rem;
cursor: pointer;
color: hsl(var(--text-secondary));
}
.drop-window:hover {
border: 2px dashed hsl(var(--border-hover));
color: hsl(var(--text));
}
.file-object {
position: relative;
display: flex;
flex-direction: row;
gap: 4px;
align-items: center;
padding: 10px 12px;
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
transition: 0.25s;
color: hsl(var(--text-secondary)) !important;
cursor: pointer;
&:hover {
border: 1px solid hsl(var(--border-hover));
color: hsl(var(--text)) !important;
}
.close {
color: hsl(var(--text-secondary)) !important;
transition: .1s;
&:hover {
color: hsl(var(--text)) !important;
}
}
}
.file-list {
display: flex;
flex-direction: column;
width: 100%;
height: max-content;
user-select: none;
margin: 1rem 0;
.file-item {
width: 100%;
height: max-content;
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 4px;
color: hsl(var(--foreground));
background: hsl(var(--background));
border-radius: var(--radius);
border: 1px solid hsl(var(--border));
padding: 0.5rem;
.file-size {
color: hsl(var(--text-secondary));
}
&:last-child {
margin-bottom: 0;
}
}
}
.file-name {
word-wrap: anywhere;
word-break: break-all;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 450px;
}
================================================
FILE: app/src/assets/common/loader.less
================================================
.loader-wrapper {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
vertical-align: center;
text-align: center;
top: 50%;
left: 50%;
gap: 20px;
transform: translate(-50%, -50%);
margin-top: -28px;
p {
text-align: center;
user-select: none;
&:after {
content: '.';
color: hsl(var(--text-secondary));
animation: dots 1s steps(5, end) infinite;
@keyframes dots {
0%, 20% {
color: rgba(0, 0, 0, 0);
text-shadow:
.25em 0 0 rgba(0, 0, 0, 0),
.5em 0 0 rgba(0, 0, 0, 0);
}
40% {
color: hsl(var(--text-secondary));
text-shadow:
.25em 0 0 rgba(0, 0, 0, 0),
.5em 0 0 rgba(0, 0, 0, 0);
}
60% {
text-shadow:
.25em 0 0 hsl(var(--text-secondary)),
.5em 0 0 rgba(0, 0, 0, 0);
}
80%, 100% {
text-shadow:
.25em 0 0 hsl(var(--text-secondary)),
.5em 0 0 hsl(var(--text-secondary));
}
}
}
}
.loader {
position: relative;
top: 0 !important;
left: 0 !important;
border: 4px solid hsl(var(--text));
border-left-color: transparent;
border-radius: 50%;
width: 46px;
height: 46px;
animation: SpinAnimation 1s linear infinite;
@keyframes SpinAnimation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
}
}
================================================
FILE: app/src/assets/common/plugin.less
================================================
.plugin-dialog {
max-width: min(95vw, 1024px) !important;
width: 100%;
height: auto;
max-height: 90vh;
}
.plugin-wrapper {
max-width: calc(95vw - 3rem);
max-height: calc(90vh - 8rem);
overflow-y: auto;
padding: 0.5rem;
padding-top: 1rem;
display: flex;
flex-direction: column;
}
.plugin-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding: 0 0.5rem;
}
.plugin-header-title {
display: flex;
align-items: center;
gap: 0.5rem;
}
.plugin-header-actions {
display: flex;
gap: 0.5rem;
}
.plugin-list {
width: 100%;
flex: 1;
& > div {
height: 100%;
}
.space-y-3 {
width: 100%;
height: auto;
padding: 0 0.5rem;
}
}
.plugin-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
transition: background-color 0.2s;
cursor: pointer;
min-height: 80px;
width: 100%;
box-sizing: border-box;
&:hover {
background-color: hsl(var(--muted-foreground) / 0.05);
}
}
.plugin-item-content {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
min-width: 0;
}
.plugin-avatar {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
flex-shrink: 0;
}
.plugin-info {
flex: 1;
min-width: 0;
}
.plugin-name {
font-weight: 500;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.plugin-description {
font-size: 0.875rem;
color: var(--muted-foreground);
margin: 0.25rem 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.plugin-url {
font-size: 0.75rem;
color: var(--muted-foreground);
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.plugin-actions {
display: flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}
.plugin-form {
padding: 1rem;
margin: 0 0.5rem;
}
.plugin-import-form {
padding: 1rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
background-color: transparent;
margin: 0 0.5rem;
transition: background-color 0.2s, border-color 0.2s;
&:hover {
background-color: hsl(var(--muted-foreground) / 0.05);
border-color: hsl(var(--border-hover));
}
}
.plugin-import-textarea {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem;
line-height: 1.5;
background-color: transparent;
border: 1px solid var(--border);
border-radius: 0.375rem;
transition: border-color 0.2s, background-color 0.2s;
&:hover {
border-color: hsl(var(--border-hover));
background-color: hsl(var(--muted-foreground) / 0.02);
}
&:focus {
border-color: hsl(var(--border-active));
background-color: hsl(var(--background));
box-shadow: 0 0 0 2px hsl(var(--border-active) / 0.2);
}
&::placeholder {
color: var(--muted-foreground);
opacity: 0.7;
}
}
.plugin-empty-state {
text-align: center;
padding: 2rem 0;
color: var(--muted-foreground);
margin: 0 0.5rem;
}
.plugin-empty-icon {
width: 3rem;
height: 3rem;
margin: 0 auto 1rem;
opacity: 0.5;
}
.plugin-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 2rem 0;
margin: 0 0.5rem;
}
/* 响应式设计 */
@media (max-width: 768px) {
.plugin-dialog {
max-width: 95vw !important;
margin: 1rem;
}
.plugin-wrapper {
max-width: calc(95vw - 2rem);
padding: 0.25rem;
padding-top: 1rem;
}
.plugin-header {
flex-direction: column;
gap: 1rem;
align-items: stretch;
text-align: center;
padding: 0;
}
.plugin-header-title {
justify-content: center;
}
.plugin-header-actions {
justify-content: center;
width: 100%;
}
.plugin-list {
height: 60vh !important;
}
.plugin-item {
flex-direction: column;
gap: 1rem;
padding: 0.75rem;
align-items: stretch;
}
.plugin-item-content {
flex-direction: column;
align-items: flex-start;
text-align: left;
}
.plugin-actions {
justify-content: center;
width: 100%;
}
.plugin-avatar {
align-self: center;
}
.plugin-info {
width: 100%;
text-align: center;
}
.plugin-name,
.plugin-description,
.plugin-url {
white-space: normal;
word-break: break-all;
}
.plugin-form {
padding: 0.75rem;
margin: 0;
}
.plugin-import-form {
padding: 0.75rem;
margin: 0;
}
.plugin-import-textarea {
font-size: 0.75rem;
}
.plugin-empty-state,
.plugin-loading {
margin: 0;
}
.plugin-form-row {
flex-direction: column;
gap: 0.75rem;
align-items: stretch;
}
.plugin-form-column {
width: 100%;
}
.plugin-avatar-picker {
align-self: center;
}
.plugin-test-scroll-area {
max-height: calc(60vh - 100px);
}
}
@media (max-width: 480px) {
.plugin-dialog {
max-width: 98vw !important;
margin: 0.5rem;
}
.plugin-wrapper {
max-width: calc(98vw - 1rem);
padding-top: 0.75rem;
}
.plugin-header {
margin-bottom: 0.75rem;
}
.plugin-item {
padding: 0.5rem;
}
.plugin-actions {
flex-wrap: wrap;
gap: 0.25rem;
}
.plugin-form {
padding: 0.5rem;
}
.plugin-import-form {
padding: 0.5rem;
}
.plugin-import-textarea {
font-size: 0.75rem;
}
}
/* Plugin Editor styles */
.plugin-drawer-viewport {
width: 100%;
max-width: 100%;
}
.plugin-form-row {
display: flex;
flex-direction: row;
gap: 1rem;
align-items: flex-end;
}
.plugin-form-column {
display: flex;
flex-direction: column;
gap: 0.5rem;
flex-shrink: 0;
}
.plugin-form-column-grow {
flex: 1;
min-width: 0;
}
.plugin-avatar-picker {
width: 2.5rem !important;
height: 2.5rem !important;
border-radius: 0.5rem;
border: 1px solid hsl(var(--border));
transition: 0.2s ease-in-out;
&:hover {
border-color: hsl(var(--border-hover));
background-color: hsl(var(--background-hover));
}
&:focus {
border-color: hsl(var(--border-active));
box-shadow: 0 0 0 2px hsl(var(--border-active) / 0.2);
}
}
.plugin-test-scroll-area {
max-height: calc(70vh - 120px);
.scrollarea-viewport {
padding-right: 0.5rem;
}
&[data-radix-scroll-area-viewport] {
max-height: 100%;
}
}
.plugin-picker-dialog {
width: max-content !important;
height: max-content !important;
padding: 0 !important;
.picker {
--epr-category-navigation-button-size: 28px;
--epr-search-input-bg-color: hsl(var(--background));
--epr-search-border-color: hsl(var(--border-hover));
--epr-bg-color: hsl(var(--background));
--epr-category-label-bg-color: hsl(var(--background));
img {
padding: 0.5rem;
}
.epr-icn-search {
width: 1rem;
height: 1rem;
transform: translateY(-0.55rem);
}
.epr-body {
scrollbar-width: thin;
-ms-overflow-style: none;
&::-webkit-scrollbar {
width: 6px;
}
}
.epr-search-container {
input {
border: 1px solid hsl(var(--border));
}
}
.epr-cat-btn {
&:focus:before {
border: none;
}
}
* {
font-family: var(--font-family) !important;
font-size: 0.85rem !important;
}
}
}
/* MCP Tool Call styles */
.mcp-container {
margin: 0.5rem 0;
padding: 0.5rem;
border-radius: 0.5rem;
background-color: rgba(var(--primary) / 0.05);
border: 1px solid rgba(var(--primary) / 0.1);
}
.mcp-tool-call {
background-color: rgba(var(--background) / 0.5);
border-radius: 0.5rem;
padding: 1rem;
margin-top: 0.5rem;
}
.tool-arguments {
background-color: rgba(var(--muted) / 0.3);
border-radius: 0.375rem;
padding: 0.75rem;
margin-bottom: 0.75rem;
border: 1px solid rgba(var(--border) / 0.5);
}
.tool-result {
border-radius: 0.375rem;
overflow: hidden;
}
.tool-result .bg-blue-500\/10 {
background-color: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
}
.tool-result .bg-green-500\/10 {
background-color: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.2);
}
.tool-result .bg-red-500\/10 {
background-color: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
}
.tool-result pre {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem;
line-height: 1.5;
max-height: 400px;
overflow-y: auto;
}
/* MCP Tool Call Button styles */
.mcp-container button {
transition: all 0.2s ease-in-out;
}
.mcp-container button:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* Status indicators */
.text-blue-500 {
color: rgb(59, 130, 246);
}
.text-green-500 {
color: rgb(34, 197, 94);
}
.text-red-500 {
color: rgb(239, 68, 68);
}
.text-blue-600 {
color: rgb(37, 99, 235);
}
.text-green-700 {
color: rgb(21, 128, 61);
}
.text-red-600 {
color: rgb(220, 38, 38);
}
/* Animation for spinner */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}
/* Responsive styles for MCP components */
@media (max-width: 768px) {
.mcp-container {
margin: 0.25rem 0;
padding: 0.375rem;
}
.mcp-tool-call {
padding: 0.75rem;
}
.tool-arguments,
.tool-result {
padding: 0.5rem;
}
.tool-result pre {
font-size: 0.75rem;
max-height: 300px;
}
}
/* MCP Debug Tabs styles */
.mcp-debug-tabs {
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
gap: 2px;
background-color: hsl(var(--muted-foreground) / 0.05);
padding: 2px;
border-radius: 0.5rem;
border: 1px solid var(--border);
}
.mcp-debug-tabs [data-state="active"] {
background-color: var(--background);
border: 1px solid var(--border);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: background-color 0.2s;
}
.mcp-debug-tabs [data-state="inactive"] {
background-color: transparent;
color: var(--muted-foreground);
transition: background-color 0.2s;
}
.mcp-debug-tabs [data-state="inactive"]:hover {
background-color: hsl(var(--muted-foreground) / 0.05);
color: var(--foreground);
}
/* Debug panel tabs content */
.debug-panel [data-orientation="horizontal"][role="tablist"] {
margin-bottom: 0.75rem;
}
.debug-panel [role="tabpanel"] {
min-height: 60px;
max-height: 400px;
}
/* MCP Debug ScrollArea styles */
.mcp-debug-scroll-area {
height: 300px;
max-height: 300px;
width: 100%;
border: 1px solid var(--border);
border-radius: 0.5rem;
background-color: hsl(var(--muted-foreground) / 0.02);
}
.mcp-debug-scroll-area [data-radix-scroll-area-viewport] {
height: 100%;
width: 100%;
border-radius: inherit;
}
.mcp-debug-scroll-area pre {
border: none;
background-color: transparent;
margin: 0;
min-height: 100%;
width: 100%;
}
/* MCP Debug Content styles (for non-scrollable content) */
.mcp-debug-content {
width: 100%;
border: 1px solid var(--border);
border-radius: 0.5rem;
background-color: hsl(var(--muted-foreground) / 0.02);
}
.mcp-debug-content pre {
border: none;
background-color: transparent;
margin: 0;
}
/* ScrollArea scrollbar styling to match plugin-item theme */
.mcp-debug-scroll-area [data-radix-scroll-area-scrollbar] {
background-color: hsl(var(--muted-foreground) / 0.05);
border-radius: 0.375rem;
transition: background-color 0.2s;
width: 8px;
height: 8px;
padding: 1px;
}
.mcp-debug-scroll-area [data-radix-scroll-area-scrollbar]:hover {
background-color: hsl(var(--muted-foreground) / 0.08);
}
.mcp-debug-scroll-area [data-radix-scroll-area-scrollbar][data-orientation="vertical"] {
width: 8px;
border-left: 1px solid transparent;
border-right: 1px solid transparent;
}
.mcp-debug-scroll-area [data-radix-scroll-area-scrollbar][data-orientation="horizontal"] {
height: 8px;
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
}
.mcp-debug-scroll-area [data-radix-scroll-area-thumb] {
background-color: hsl(var(--muted-foreground) / 0.3);
border-radius: 0.25rem;
transition: background-color 0.2s;
position: relative;
}
.mcp-debug-scroll-area [data-radix-scroll-area-thumb]:hover {
background-color: hsl(var(--muted-foreground) / 0.5);
}
.mcp-debug-scroll-area [data-radix-scroll-area-corner] {
background-color: hsl(var(--muted-foreground) / 0.05);
}
/* Responsive styles for debug tabs */
@media (max-width: 768px) {
.mcp-debug-tabs {
grid-template-columns: repeat(auto-fit, minmax(60px, 1fr));
gap: 1px;
padding: 1px;
}
.debug-panel [role="tabpanel"] {
min-height: 40px;
max-height: 250px;
}
.mcp-debug-scroll-area {
height: 180px;
max-height: 180px;
}
.mcp-debug-scroll-area pre,
.mcp-debug-content pre {
font-size: 0.75rem;
}
}
@media (max-width: 480px) {
.mcp-debug-tabs {
grid-template-columns: repeat(auto-fit, minmax(50px, 1fr));
}
.debug-panel [role="tabpanel"] {
min-height: 30px;
max-height: 180px;
}
.mcp-debug-scroll-area {
height: 120px;
max-height: 120px;
}
.mcp-debug-scroll-area pre,
.mcp-debug-content pre {
padding: 0.5rem;
}
}
================================================
FILE: app/src/assets/fonts/all.less
================================================
@import "common";
@import "katex";
================================================
FILE: app/src/assets/fonts/common.less
================================================
@import '@fontsource-variable/inter';
@import '@fontsource-variable/jetbrains-mono';
================================================
FILE: app/src/assets/fonts/katex.less
================================================
/* stylelint-disable font-family-no-missing-generic-family-keyword */
@font-face {
font-family: 'KaTeX_AMS';
src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_AMS-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_AMS-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_AMS-Regular.ttf) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Caligraphic';
src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Caligraphic-Bold.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Caligraphic-Bold.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Caligraphic-Bold.ttf) format('truetype');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Caligraphic';
src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Caligraphic-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Caligraphic-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Caligraphic-Regular.ttf) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Fraktur';
src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Fraktur-Bold.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Fraktur-Bold.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Fraktur-Bold.ttf) format('truetype');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Fraktur';
src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Fraktur-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Fraktur-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Fraktur-Regular.ttf) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Main';
src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Bold.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Bold.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Bold.ttf) format('truetype');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Main';
src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-BoldItalic.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-BoldItalic.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-BoldItalic.ttf) format('truetype');
font-weight: bold;
font-style: italic;
}
@font-face {
font-family: 'KaTeX_Main';
src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Italic.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Italic.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Italic.ttf) format('truetype');
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: 'KaTeX_Main';
src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Main-Regular.ttf) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Math';
src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Math-BoldItalic.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Math-BoldItalic.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Math-BoldItalic.ttf) format('truetype');
font-weight: bold;
font-style: italic;
}
@font-face {
font-family: 'KaTeX_Math';
src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Math-Italic.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Math-Italic.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Math-Italic.ttf) format('truetype');
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: 'KaTeX_SansSerif';
src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Bold.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Bold.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Bold.ttf) format('truetype');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_SansSerif';
src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Italic.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Italic.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Italic.ttf) format('truetype');
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: 'KaTeX_SansSerif';
src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_SansSerif-Regular.ttf) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Script';
src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Script-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Script-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Script-Regular.ttf) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Size1';
src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size1-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size1-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size1-Regular.ttf) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Size2';
src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size2-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size2-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size2-Regular.ttf) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Size3';
src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size3-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size3-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size3-Regular.ttf) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Size4';
src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size4-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size4-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Size4-Regular.ttf) format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Typewriter';
src: url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Typewriter-Regular.woff2) format('woff2'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Typewriter-Regular.woff) format('woff'), url(https://cdn.bootcdn.net/ajax/libs/KaTeX/0.16.0/fonts/KaTeX_Typewriter-Regular.ttf) format('truetype');
font-weight: normal;
font-style: normal;
}
.katex {
font: normal 1.21em KaTeX_Main, Times New Roman, serif;
line-height: 1.2;
text-indent: 0;
text-rendering: auto;
}
.katex * {
-ms-high-contrast-adjust: none !important;
border-color: currentColor;
}
.katex .katex-version::after {
content: "0.16.0";
}
.katex .katex-mathml {
/* Accessibility hack to only show to screen readers
Found at: http://a11yproject.com/posts/how-to-hide-content/ */
position: absolute;
clip: rect(1px, 1px, 1px, 1px);
padding: 0;
border: 0;
height: 1px;
width: 1px;
overflow: hidden;
}
.katex .katex-html {
/* \newline is an empty block at top level, between .base elements */
white-space: pre-wrap;
}
.katex .katex-html > .newline {
display: block;
}
.katex .base {
position: relative;
display: inline-block;
}
.katex .strut {
display: inline-block;
}
.katex .textbf {
font-weight: bold;
}
.katex .textit {
font-style: italic;
}
.katex .textrm {
font-family: KaTeX_Main;
}
.katex .textsf {
font-family: KaTeX_SansSerif;
}
.katex .texttt {
font-family: KaTeX_Typewriter;
}
.katex .mathnormal {
font-family: KaTeX_Math;
font-style: italic;
}
.katex .mathit {
font-family: KaTeX_Main;
font-style: italic;
}
.katex .mathrm {
font-style: normal;
}
.katex .mathbf {
font-family: KaTeX_Main;
font-weight: bold;
}
.katex .boldsymbol {
font-family: KaTeX_Math;
font-weight: bold;
font-style: italic;
}
.katex .amsrm {
font-family: KaTeX_AMS;
}
.katex .mathbb,
.katex .textbb {
font-family: KaTeX_AMS;
}
.katex .mathcal {
font-family: KaTeX_Caligraphic;
}
.katex .mathfrak,
.katex .textfrak {
font-family: KaTeX_Fraktur;
}
.katex .mathtt {
font-family: KaTeX_Typewriter;
}
.katex .mathscr,
.katex .textscr {
font-family: KaTeX_Script;
}
.katex .mathsf,
.katex .textsf {
font-family: KaTeX_SansSerif;
}
.katex .mathboldsf,
.katex .textboldsf {
font-family: KaTeX_SansSerif;
font-weight: bold;
}
.katex .mathitsf,
.katex .textitsf {
font-family: KaTeX_SansSerif;
font-style: italic;
}
.katex .mainrm {
font-family: KaTeX_Main;
font-style: normal;
}
.katex .vlist-t {
display: inline-table;
table-layout: fixed;
border-collapse: collapse;
}
.katex .vlist-r {
display: table-row;
}
.katex .vlist {
display: table-cell;
vertical-align: bottom;
position: relative;
}
.katex .vlist > span {
display: block;
height: 0;
position: relative;
}
.katex .vlist > span > span {
display: inline-block;
}
.katex .vlist > span > .pstrut {
overflow: hidden;
width: 0;
}
.katex .vlist-t2 {
margin-right: -2px;
}
.katex .vlist-s {
display: table-cell;
vertical-align: bottom;
font-size: 1px;
width: 2px;
min-width: 2px;
}
.katex .vbox {
display: inline-flex;
flex-direction: column;
align-items: baseline;
}
.katex .hbox {
display: inline-flex;
flex-direction: row;
width: 100%;
}
.katex .thinbox {
display: inline-flex;
flex-direction: row;
width: 0;
max-width: 0;
}
.katex .msupsub {
text-align: left;
}
.katex .mfrac > span > span {
text-align: center;
}
.katex .mfrac .frac-line {
display: inline-block;
width: 100%;
border-bottom-style: solid;
}
.katex .mfrac .frac-line,
.katex .overline .overline-line,
.katex .underline .underline-line,
.katex .hline,
.katex .hdashline,
.katex .rule {
min-height: 1px;
}
.katex .mspace {
display: inline-block;
}
.katex .llap,
.katex .rlap,
.katex .clap {
width: 0;
position: relative;
}
.katex .llap > .inner,
.katex .rlap > .inner,
.katex .clap > .inner {
position: absolute;
}
.katex .llap > .fix,
.katex .rlap > .fix,
.katex .clap > .fix {
display: inline-block;
}
.katex .llap > .inner {
right: 0;
}
.katex .rlap > .inner,
.katex .clap > .inner {
left: 0;
}
.katex .clap > .inner > span {
margin-left: -50%;
margin-right: 50%;
}
.katex .rule {
display: inline-block;
border: solid 0;
position: relative;
}
.katex .overline .overline-line,
.katex .underline .underline-line,
.katex .hline {
display: inline-block;
width: 100%;
border-bottom-style: solid;
}
.katex .hdashline {
display: inline-block;
width: 100%;
border-bottom-style: dashed;
}
.katex .sqrt > .root {
/* These values are taken from the definition of `\r@@t`,
`\mkern 5mu` and `\mkern -10mu`. */
margin-left: 0.27777778em;
margin-right: -0.55555556em;
}
.katex .sizing.reset-size1.size1,
.katex .fontsize-ensurer.reset-size1.size1 {
font-size: 1em;
}
.katex .sizing.reset-size1.size2,
.katex .fontsize-ensurer.reset-size1.size2 {
font-size: 1.2em;
}
.katex .sizing.reset-size1.size3,
.katex .fontsize-ensurer.reset-size1.size3 {
font-size: 1.4em;
}
.katex .sizing.reset-size1.size4,
.katex .fontsize-ensurer.reset-size1.size4 {
font-size: 1.6em;
}
.katex .sizing.reset-size1.size5,
.katex .fontsize-ensurer.reset-size1.size5 {
font-size: 1.8em;
}
.katex .sizing.reset-size1.size6,
.katex .fontsize-ensurer.reset-size1.size6 {
font-size: 2em;
}
.katex .sizing.reset-size1.size7,
.katex .fontsize-ensurer.reset-size1.size7 {
font-size: 2.4em;
}
.katex .sizing.reset-size1.size8,
.katex .fontsize-ensurer.reset-size1.size8 {
font-size: 2.88em;
}
.katex .sizing.reset-size1.size9,
.katex .fontsize-ensurer.reset-size1.size9 {
font-size: 3.456em;
}
.katex .sizing.reset-size1.size10,
.katex .fontsize-ensurer.reset-size1.size10 {
font-size: 4.148em;
}
.katex .sizing.reset-size1.size11,
.katex .fontsize-ensurer.reset-size1.size11 {
font-size: 4.976em;
}
.katex .sizing.reset-size2.size1,
.katex .fontsize-ensurer.reset-size2.size1 {
font-size: 0.83333333em;
}
.katex .sizing.reset-size2.size2,
.katex .fontsize-ensurer.reset-size2.size2 {
font-size: 1em;
}
.katex .sizing.reset-size2.size3,
.katex .fontsize-ensurer.reset-size2.size3 {
font-size: 1.16666667em;
}
.katex .sizing.reset-size2.size4,
.katex .fontsize-ensurer.reset-size2.size4 {
font-size: 1.33333333em;
}
.katex .sizing.reset-size2.size5,
.katex .fontsize-ensurer.reset-size2.size5 {
font-size: 1.5em;
}
.katex .sizing.reset-size2.size6,
.katex .fontsize-ensurer.reset-size2.size6 {
font-size: 1.66666667em;
}
.katex .sizing.reset-size2.size7,
.katex .fontsize-ensurer.reset-size2.size7 {
font-size: 2em;
}
.katex .sizing.reset-size2.size8,
.katex .fontsize-ensurer.reset-size2.size8 {
font-size: 2.4em;
}
.katex .sizing.reset-size2.size9,
.katex .fontsize-ensurer.reset-size2.size9 {
font-size: 2.88em;
}
.katex .sizing.reset-size2.size10,
.katex .fontsize-ensurer.reset-size2.size10 {
font-size: 3.45666667em;
}
.katex .sizing.reset-size2.size11,
.katex .fontsize-ensurer.reset-size2.size11 {
font-size: 4.14666667em;
}
.katex .sizing.reset-size3.size1,
.katex .fontsize-ensurer.reset-size3.size1 {
font-size: 0.71428571em;
}
.katex .sizing.reset-size3.size2,
.katex .fontsize-ensurer.reset-size3.size2 {
font-size: 0.85714286em;
}
.katex .sizing.reset-size3.size3,
.katex .fontsize-ensurer.reset-size3.size3 {
font-size: 1em;
}
.katex .sizing.reset-size3.size4,
.katex .fontsize-ensurer.reset-size3.size4 {
font-size: 1.14285714em;
}
.katex .sizing.reset-size3.size5,
.katex .fontsize-ensurer.reset-size3.size5 {
font-size: 1.28571429em;
}
.katex .sizing.reset-size3.size6,
.katex .fontsize-ensurer.reset-size3.size6 {
font-size: 1.42857143em;
}
.katex .sizing.reset-size3.size7,
.katex .fontsize-ensurer.reset-size3.size7 {
font-size: 1.71428571em;
}
.katex .sizing.reset-size3.size8,
.katex .fontsize-ensurer.reset-size3.size8 {
font-size: 2.05714286em;
}
.katex .sizing.reset-size3.size9,
.katex .fontsize-ensurer.reset-size3.size9 {
font-size: 2.46857143em;
}
.katex .sizing.reset-size3.size10,
.katex .fontsize-ensurer.reset-size3.size10 {
font-size: 2.96285714em;
}
.katex .sizing.reset-size3.size11,
.katex .fontsize-ensurer.reset-size3.size11 {
font-size: 3.55428571em;
}
.katex .sizing.reset-size4.size1,
.katex .fontsize-ensurer.reset-size4.size1 {
font-size: 0.625em;
}
.katex .sizing.reset-size4.size2,
.katex .fontsize-ensurer.reset-size4.size2 {
font-size: 0.75em;
}
.katex .sizing.reset-size4.size3,
.katex .fontsize-ensurer.reset-size4.size3 {
font-size: 0.875em;
}
.katex .sizing.reset-size4.size4,
.katex .fontsize-ensurer.reset-size4.size4 {
font-size: 1em;
}
.katex .sizing.reset-size4.size5,
.katex .fontsize-ensurer.reset-size4.size5 {
font-size: 1.125em;
}
.katex .sizing.reset-size4.size6,
.katex .fontsize-ensurer.reset-size4.size6 {
font-size: 1.25em;
}
.katex .sizing.reset-size4.size7,
.katex .fontsize-ensurer.reset-size4.size7 {
font-size: 1.5em;
}
.katex .sizing.reset-size4.size8,
.katex .fontsize-ensurer.reset-size4.size8 {
font-size: 1.8em;
}
.katex .sizing.reset-size4.size9,
.katex .fontsize-ensurer.reset-size4.size9 {
font-size: 2.16em;
}
.katex .sizing.reset-size4.size10,
.katex .fontsize-ensurer.reset-size4.size10 {
font-size: 2.5925em;
}
.katex .sizing.reset-size4.size11,
.katex .fontsize-ensurer.reset-size4.size11 {
font-size: 3.11em;
}
.katex .sizing.reset-size5.size1,
.katex .fontsize-ensurer.reset-size5.size1 {
font-size: 0.55555556em;
}
.katex .sizing.reset-size5.size2,
.katex .fontsize-ensurer.reset-size5.size2 {
font-size: 0.66666667em;
}
.katex .sizing.reset-size5.size3,
.katex .fontsize-ensurer.reset-size5.size3 {
font-size: 0.77777778em;
}
.katex .sizing.reset-size5.size4,
.katex .fontsize-ensurer.reset-size5.size4 {
font-size: 0.88888889em;
}
.katex .sizing.reset-size5.size5,
.katex .fontsize-ensurer.reset-size5.size5 {
font-size: 1em;
}
.katex .sizing.reset-size5.size6,
.katex .fontsize-ensurer.reset-size5.size6 {
font-size: 1.11111111em;
}
.katex .sizing.reset-size5.size7,
.katex .fontsize-ensurer.reset-size5.size7 {
font-size: 1.33333333em;
}
.katex .sizing.reset-size5.size8,
.katex .fontsize-ensurer.reset-size5.size8 {
font-size: 1.6em;
}
.katex .sizing.reset-size5.size9,
.katex .fontsize-ensurer.reset-size5.size9 {
font-size: 1.92em;
}
.katex .sizing.reset-size5.size10,
.katex .fontsize-ensurer.reset-size5.size10 {
font-size: 2.30444444em;
}
.katex .sizing.reset-size5.size11,
.katex .fontsize-ensurer.reset-size5.size11 {
font-size: 2.76444444em;
}
.katex .sizing.reset-size6.size1,
.katex .fontsize-ensurer.reset-size6.size1 {
font-size: 0.5em;
}
.katex .sizing.reset-size6.size2,
.katex .fontsize-ensurer.reset-size6.size2 {
font-size: 0.6em;
}
.katex .sizing.reset-size6.size3,
.katex .fontsize-ensurer.reset-size6.size3 {
font-size: 0.7em;
}
.katex .sizing.reset-size6.size4,
.katex .fontsize-ensurer.reset-size6.size4 {
font-size: 0.8em;
}
.katex .sizing.reset-size6.size5,
.katex .fontsize-ensurer.reset-size6.size5 {
font-size: 0.9em;
}
.katex .sizing.reset-size6.size6,
.katex .fontsize-ensurer.reset-size6.size6 {
font-size: 1em;
}
.katex .sizing.reset-size6.size7,
.katex .fontsize-ensurer.reset-size6.size7 {
font-size: 1.2em;
}
.katex .sizing.reset-size6.size8,
.katex .fontsize-ensurer.reset-size6.size8 {
font-size: 1.44em;
}
.katex .sizing.reset-size6.size9,
.katex .fontsize-ensurer.reset-size6.size9 {
font-size: 1.728em;
}
.katex .sizing.reset-size6.size10,
.katex .fontsize-ensurer.reset-size6.size10 {
font-size: 2.074em;
}
.katex .sizing.reset-size6.size11,
.katex .fontsize-ensurer.reset-size6.size11 {
font-size: 2.488em;
}
.katex .sizing.reset-size7.size1,
.katex .fontsize-ensurer.reset-size7.size1 {
font-size: 0.41666667em;
}
.katex .sizing.reset-size7.size2,
.katex .fontsize-ensurer.reset-size7.size2 {
font-size: 0.5em;
}
.katex .sizing.reset-size7.size3,
.katex .fontsize-ensurer.reset-size7.size3 {
font-size: 0.58333333em;
}
.katex .sizing.reset-size7.size4,
.katex .fontsize-ensurer.reset-size7.size4 {
font-size: 0.66666667em;
}
.katex .sizing.reset-size7.size5,
.katex .fontsize-ensurer.reset-size7.size5 {
font-size: 0.75em;
}
.katex .sizing.reset-size7.size6,
.katex .fontsize-ensurer.reset-size7.size6 {
font-size: 0.83333333em;
}
.katex .sizing.reset-size7.size7,
.katex .fontsize-ensurer.reset-size7.size7 {
font-size: 1em;
}
.katex .sizing.reset-size7.size8,
.katex .fontsize-ensurer.reset-size7.size8 {
font-size: 1.2em;
}
.katex .sizing.reset-size7.size9,
.katex .fontsize-ensurer.reset-size7.size9 {
font-size: 1.44em;
}
.katex .sizing.reset-size7.size10,
.katex .fontsize-ensurer.reset-size7.size10 {
font-size: 1.72833333em;
}
.katex .sizing.reset-size7.size11,
.katex .fontsize-ensurer.reset-size7.size11 {
font-size: 2.07333333em;
}
.katex .sizing.reset-size8.size1,
.katex .fontsize-ensurer.reset-size8.size1 {
font-size: 0.34722222em;
}
.katex .sizing.reset-size8.size2,
.katex .fontsize-ensurer.reset-size8.size2 {
font-size: 0.41666667em;
}
.katex .sizing.reset-size8.size3,
.katex .fontsize-ensurer.reset-size8.size3 {
font-size: 0.48611111em;
}
.katex .sizing.reset-size8.size4,
.katex .fontsize-ensurer.reset-size8.size4 {
font-size: 0.55555556em;
}
.katex .sizing.reset-size8.size5,
.katex .fontsize-ensurer.reset-size8.size5 {
font-size: 0.625em;
}
.katex .sizing.reset-size8.size6,
.katex .fontsize-ensurer.reset-size8.size6 {
font-size: 0.69444444em;
}
.katex .sizing.reset-size8.size7,
.katex .fontsize-ensurer.reset-size8.size7 {
font-size: 0.83333333em;
}
.katex .sizing.reset-size8.size8,
.katex .fontsize-ensurer.reset-size8.size8 {
font-size: 1em;
}
.katex .sizing.reset-size8.size9,
.katex .fontsize-ensurer.reset-size8.size9 {
font-size: 1.2em;
}
.katex .sizing.reset-size8.size10,
.katex .fontsize-ensurer.reset-size8.size10 {
font-size: 1.44027778em;
}
.katex .sizing.reset-size8.size11,
.katex .fontsize-ensurer.reset-size8.size11 {
font-size: 1.72777778em;
}
.katex .sizing.reset-size9.size1,
.katex .fontsize-ensurer.reset-size9.size1 {
font-size: 0.28935185em;
}
.katex .sizing.reset-size9.size2,
.katex .fontsize-ensurer.reset-size9.size2 {
font-size: 0.34722222em;
}
.katex .sizing.reset-size9.size3,
.katex .fontsize-ensurer.reset-size9.size3 {
font-size: 0.40509259em;
}
.katex .sizing.reset-size9.size4,
.katex .fontsize-ensurer.reset-size9.size4 {
font-size: 0.46296296em;
}
.katex .sizing.reset-size9.size5,
.katex .fontsize-ensurer.reset-size9.size5 {
font-size: 0.52083333em;
}
.katex .sizing.reset-size9.size6,
.katex .fontsize-ensurer.reset-size9.size6 {
font-size: 0.5787037em;
}
.katex .sizing.reset-size9.size7,
.katex .fontsize-ensurer.reset-size9.size7 {
font-size: 0.69444444em;
}
.katex .sizing.reset-size9.size8,
.katex .fontsize-ensurer.reset-size9.size8 {
font-size: 0.83333333em;
}
.katex .sizing.reset-size9.size9,
.katex .fontsize-ensurer.reset-size9.size9 {
font-size: 1em;
}
.katex .sizing.reset-size9.size10,
.katex .fontsize-ensurer.reset-size9.size10 {
font-size: 1.20023148em;
}
.katex .sizing.reset-size9.size11,
.katex .fontsize-ensurer.reset-size9.size11 {
font-size: 1.43981481em;
}
.katex .sizing.reset-size10.size1,
.katex .fontsize-ensurer.reset-size10.size1 {
font-size: 0.24108004em;
}
.katex .sizing.reset-size10.size2,
.katex .fontsize-ensurer.reset-size10.size2 {
font-size: 0.28929605em;
}
.katex .sizing.reset-size10.size3,
.katex .fontsize-ensurer.reset-size10.size3 {
font-size: 0.33751205em;
}
.katex .sizing.reset-size10.size4,
.katex .fontsize-ensurer.reset-size10.size4 {
font-size: 0.38572806em;
}
.katex .sizing.reset-size10.size5,
.katex .fontsize-ensurer.reset-size10.size5 {
font-size: 0.43394407em;
}
.katex .sizing.reset-size10.size6,
.katex .fontsize-ensurer.reset-size10.size6 {
font-size: 0.48216008em;
}
.katex .sizing.reset-size10.size7,
.katex .fontsize-ensurer.reset-size10.size7 {
font-size: 0.57859209em;
}
.katex .sizing.reset-size10.size8,
.katex .fontsize-ensurer.reset-size10.size8 {
font-size: 0.69431051em;
}
.katex .sizing.reset-size10.size9,
.katex .fontsize-ensurer.reset-size10.size9 {
font-size: 0.83317261em;
}
.katex .sizing.reset-size10.size10,
.katex .fontsize-ensurer.reset-size10.size10 {
font-size: 1em;
}
.katex .sizing.reset-size10.size11,
.katex .fontsize-ensurer.reset-size10.size11 {
font-size: 1.19961427em;
}
.katex .sizing.reset-size11.size1,
.katex .fontsize-ensurer.reset-size11.size1 {
font-size: 0.20096463em;
}
.katex .sizing.reset-size11.size2,
.katex .fontsize-ensurer.reset-size11.size2 {
font-size: 0.24115756em;
}
.katex .sizing.reset-size11.size3,
.katex .fontsize-ensurer.reset-size11.size3 {
font-size: 0.28135048em;
}
.katex .sizing.reset-size11.size4,
.katex .fontsize-ensurer.reset-size11.size4 {
font-size: 0.32154341em;
}
.katex .sizing.reset-size11.size5,
.katex .fontsize-ensurer.reset-size11.size5 {
font-size: 0.36173633em;
}
.katex .sizing.reset-size11.size6,
.katex .fontsize-ensurer.reset-size11.size6 {
font-size: 0.40192926em;
}
.katex .sizing.reset-size11.size7,
.katex .fontsize-ensurer.reset-size11.size7 {
font-size: 0.48231511em;
}
.katex .sizing.reset-size11.size8,
.katex .fontsize-ensurer.reset-size11.size8 {
font-size: 0.57877814em;
}
.katex .sizing.reset-size11.size9,
.katex .fontsize-ensurer.reset-size11.size9 {
font-size: 0.69453376em;
}
.katex .sizing.reset-size11.size10,
.katex .fontsize-ensurer.reset-size11.size10 {
font-size: 0.83360129em;
}
.katex .sizing.reset-size11.size11,
.katex .fontsize-ensurer.reset-size11.size11 {
font-size: 1em;
}
.katex .delimsizing.size1 {
font-family: KaTeX_Size1;
}
.katex .delimsizing.size2 {
font-family: KaTeX_Size2;
}
.katex .delimsizing.size3 {
font-family: KaTeX_Size3;
}
.katex .delimsizing.size4 {
font-family: KaTeX_Size4;
}
.katex .delimsizing.mult .delim-size1 > span {
font-family: KaTeX_Size1;
}
.katex .delimsizing.mult .delim-size4 > span {
font-family: KaTeX_Size4;
}
.katex .nulldelimiter {
display: inline-block;
width: 0.12em;
}
.katex .delimcenter {
position: relative;
}
.katex .op-symbol {
position: relative;
}
.katex .op-symbol.small-op {
font-family: KaTeX_Size1;
}
.katex .op-symbol.large-op {
font-family: KaTeX_Size2;
}
.katex .op-limits > .vlist-t {
text-align: center;
}
.katex .accent > .vlist-t {
text-align: center;
}
.katex .accent .accent-body {
position: relative;
}
.katex .accent .accent-body:not(.accent-full) {
width: 0;
}
.katex .overlay {
display: block;
}
.katex .mtable .vertical-separator {
display: inline-block;
min-width: 1px;
}
.katex .mtable .arraycolsep {
display: inline-block;
}
.katex .mtable .col-align-c > .vlist-t {
text-align: center;
}
.katex .mtable .col-align-l > .vlist-t {
text-align: left;
}
.katex .mtable .col-align-r > .vlist-t {
text-align: right;
}
.katex .svg-align {
text-align: left;
}
.katex svg {
display: block;
position: absolute;
width: 100%;
height: inherit;
fill: currentColor;
stroke: currentColor;
fill-rule: nonzero;
fill-opacity: 1;
stroke-width: 1;
stroke-linecap: butt;
stroke-linejoin: miter;
stroke-miterlimit: 4;
stroke-dasharray: none;
stroke-dashoffset: 0;
stroke-opacity: 1;
}
.katex svg path {
stroke: none;
}
.katex img {
border-style: none;
min-width: 0;
min-height: 0;
max-width: none;
max-height: none;
}
.katex .stretchy {
width: 100%;
display: block;
position: relative;
overflow: hidden;
}
.katex .stretchy::before,
.katex .stretchy::after {
content: "";
}
.katex .hide-tail {
width: 100%;
position: relative;
overflow: hidden;
}
.katex .halfarrow-left {
position: absolute;
left: 0;
width: 50.2%;
overflow: hidden;
}
.katex .halfarrow-right {
position: absolute;
right: 0;
width: 50.2%;
overflow: hidden;
}
.katex .brace-left {
position: absolute;
left: 0;
width: 25.1%;
overflow: hidden;
}
.katex .brace-center {
position: absolute;
left: 25%;
width: 50%;
overflow: hidden;
}
.katex .brace-right {
position: absolute;
right: 0;
width: 25.1%;
overflow: hidden;
}
.katex .x-arrow-pad {
padding: 0 0.5em;
}
.katex .cd-arrow-pad {
padding: 0 0.55556em 0 0.27778em;
}
.katex .x-arrow,
.katex .mover,
.katex .munder {
text-align: center;
}
.katex .boxpad {
padding: 0 0.3em;
}
.katex .fbox,
.katex .fcolorbox {
box-sizing: border-box;
border: 0.04em solid;
}
.katex .cancel-pad {
padding: 0 0.2em;
}
.katex .cancel-lap {
margin-left: -0.2em;
margin-right: -0.2em;
}
.katex .sout {
border-bottom-style: solid;
border-bottom-width: 0.08em;
}
.katex .angl {
box-sizing: border-box;
border-top: 0.049em solid;
border-right: 0.049em solid;
margin-right: 0.03889em;
}
.katex .anglpad {
padding: 0 0.03889em;
}
.katex .eqn-num::before {
counter-increment: katexEqnNo;
content: "(" counter(katexEqnNo) ")";
}
.katex .mml-eqn-num::before {
counter-increment: mmlEqnNo;
content: "(" counter(mmlEqnNo) ")";
}
.katex .mtr-glue {
width: 50%;
}
.katex .cd-vert-arrow {
display: inline-block;
position: relative;
}
.katex .cd-label-left {
display: inline-block;
position: absolute;
right: calc(50% + 0.3em);
text-align: left;
}
.katex .cd-label-right {
display: inline-block;
position: absolute;
left: calc(50% + 0.3em);
text-align: right;
}
.katex-display {
display: block;
margin: 1em 0;
text-align: center;
}
.katex-display > .katex {
display: block;
text-align: center;
white-space: nowrap;
}
.katex-display > .katex > .katex-html {
display: block;
position: relative;
}
.katex-display > .katex > .katex-html > .tag {
position: absolute;
right: 0;
}
.katex-display.leqno > .katex > .katex-html > .tag {
left: 0;
right: auto;
}
.katex-display.fleqn > .katex {
text-align: left;
padding-left: 2em;
}
body {
counter-reset: katexEqnNo mmlEqnNo;
}
================================================
FILE: app/src/assets/globals.less
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
@import 'normalize.css';
@layer base {
:root {
--background: 0 0% 100%;
--background-light: 0 0% 100%;
--background-dark: 0 0% 0%;
--background-hover: 5 0 98%;
--background-active: 5 0 96%;
--background-container: 0, 0%, 97%, 0.9;
--background-container-hover: 0, 0%, 97%, 0.95;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-hover: 0 0% 97%;
--card-active: 0 0% 94.5%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 5.9% 97.5%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-secondary: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 67.22% 50.59%;
--destructive-foreground: 0 0% 98%;
--success: 120 100% 50%;
--success-foreground: 0 0% 98%;
--failure: 0 67.22% 50.59%;
--failure-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--border-hover: 240 5.9% 85%;
--border-active: 240 5.9% 80%;
--input: 240 5.9% 90%;
--input-unread: 240 5.9% 50%;
--ring: 240 5% 64.9%;
--text: 0 0% 0%;
--text-light: 0 0% 100%;
--text-dark: 0 0% 100%;
--text-secondary: 0 0% 35%;
--text-unread: 0 0% 50%;
--text-secondary-dark: 0 0% 80%;
--selection: 212 100% 41%;
--selection-foreground: 0 0% 98%;
--radius: 0.5rem;
--shadow: #00000005;
--gold: 40 100% 50%;
--gold-foreground: 35 100% 40%;
--link: 210 100% 63%;
--error: 20 80% 50%;
--anim-bar: no-repeat linear-gradient(#333 0 0);
}
.dark {
--background: 0 0% 0%;
--background-hover: 0 0% 7.8%;
--background-active: 0 0% 13.7%;
--background-container: 0, 0%, 5%, 0.9;
--background-container-hover: 0, 0%, 5%, 0.95;
--foreground: 210 40% 98%;
--card: 240 10% 3.9%;
--card-hover: 240 11% 12.5%;
--card-active: 240 9.5% 19%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 0 0 10%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-secondary: 240 5% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 240 3.7% 12.9%;
--border-hover: 240 3.7% 17.9%;
--border-active: 240 3.7% 25.9%;
--input: 240 3.7% 15.9%;
--input-unread: 240 3.7% 50%;
--ring: 240 4.9% 83.9%;
--text: 0 0% 100%;
--text-secondary: 0 0% 80%;
--text-unread: 0 0% 50%;
--shadow: #ffffff05;
--anim-bar: no-repeat linear-gradient(#ccc 0 0);
}
[type='text']:focus, input:where(:not([type])):focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus {
@apply border-border;
}
}
* {
--tw-ring-color: none !important;
}
.ui-input:-webkit-autofill,
.ui-input:-moz-autofill,
.ui-input:-ms-autofill,
.ui-input:-o-autofill,
.ui-input:autofill {
background: hsl(var(--background)) !important;
color: hsl(var(--foreground)) !important;
-webkit-text-fill-color: hsl(var(--foreground)) !important;
transition: background-color 5000s ease-in-out 0s;
caret-color: hsl(var(--foreground)) !important;
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
================================================
FILE: app/src/assets/main.less
================================================
@import "ui";
@font-family: "HarmonyOS Sans","Inter Variable",ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
@font-family-normal: "HarmonyOS Sans","Inter Variable",ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
@font-family-code: "JetBrains Mono Variable","HarmonyOS Sans",monospace,"Inter Variable",ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
@line-height: 1.5;
@font-weight: 400;
@color-scheme: light dark;
@font-synthesis: none;
@text-rendering: optimizeLegibility;
@-webkit-font-smoothing: antialiased;
@-moz-osx-font-smoothing: grayscale;
@-webkit-text-size-adjust: 100%;
html, body, #root {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
touch-action: pan-y;
-webkit-overflow-scrolling: touch;
}
:root {
font-family: @font-family;
line-height: @line-height;
font-weight: @font-weight;
color-scheme: @color-scheme;
font-synthesis: @font-synthesis;
text-rendering: @text-rendering;
-webkit-font-smoothing: @-webkit-font-smoothing;
-moz-osx-font-smoothing: @-moz-osx-font-smoothing;
-webkit-text-size-adjust: @-webkit-text-size-adjust;
--font-family: @font-family;
--font-family-normal: @font-family-normal;
--font-family-code: @font-family-code;
--navbar-height: 3.375rem;
}
.font-code {
font-family: @font-family-code;
}
* {
outline: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
-webkit-overflow-scrolling: touch;
}
body {
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.grow {
flex-grow: 1;
}
strong {
font-weight: bold;
}
.jetbrains-mono {
font-family: @font-family-code;
}
.hover\:bg-accent[aria-pressed="false"] {
color: hsl(var(--text-secondary));
&:hover {
color: hsl(var(--text-secondary));
background: hsl(var(--accent-secondary));
}
}
.hover\:bg-accent[aria-pressed="true"] {
background: hsl(var(--accent));
color: hsl(var(--text));
border: 1px solid hsl(var(--border));
}
.icon-tooltip {
display: flex;
flex-direction: row;
align-items: center;
&.gold {
color: hsl(var(--gold));
}
}
.flex-dialog {
border-radius: var(--radius) !important;
max-height: calc(95% - 2rem) !important;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
outline: none;
@media (max-width: 520px) {
& {
margin-top: 1rem;
margin-bottom: 1rem;
transform: translate(var(--tw-translate-x), calc(var(--tw-translate-y) - 1rem)) !important;
}
}
.link {
color: hsl(var(--text-secondary));
text-decoration: none;
transition: color 0.2s ease-in-out;
user-select: none;
cursor: pointer;
margin: 0.5rem auto;
&:hover {
color: hsl(var(--text));
}
}
.content-h-60 {
height: 60vh;
}
.content-w-980 {
width: 80vw;
max-width: 980px;
}
&.full-screen {
width: 100vw !important;
height: 100% !important;
max-width: 100vw !important;
max-height: 100% !important;
border-radius: 0 !important;
border: 1px solid hsl(var(--border)) !important;
.content-h-60 {
height: calc(100vh - 6rem);
}
.content-w-980 {
max-width: none;
width: calc(100vw - 2rem);
}
.content-h-fit {
height: fit-content;
}
.editor-preview,
.editor-input {
min-height: calc(100vh - 8.5rem) !important;
}
}
}
.announcement-dialog {
max-width: min(90vw, 720px) !important;
}
.share-dialog {
max-width: min(90vw, 720px) !important;
}
.record-dialog {
max-width: min(90vw, 1024px) !important;
}
.pre-quota-dialog {
max-width: min(90vw, 720px) !important;
}
.key-dialog {
max-width: min(90vw, 1024px) !important;
}
.fixed-dialog {
border-radius: var(--radius) !important;
max-height: calc(95% - 2rem) !important;
min-height: 60vh;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
outline: 0;
@media (max-width: 660px) {
width: 100vw !important;
height: 100% !important;
max-width: 100vw !important;
max-height: 100% !important;
border-radius: 0 !important;
}
}
.cent {
font-weight: normal !important;
}
.error-boundary {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: max-content;
min-height: calc(100% - var(--navbar-height));
overflow: hidden;
background: hsla(var(--background-container));
padding: 2.5rem 5rem;
@media (max-width: 720px) {
& {
padding: 2.5rem 1rem;
}
}
.error-provider {
text-align: center;
margin: 0.5rem auto;
p {
margin: 0.35rem;
}
}
.error-tips {
text-align: center;
padding: 1rem 2rem;
color: hsl(var(--text-secondary));
}
}
.tips-icon {
width: 1rem;
height: 1rem;
cursor: pointer;
margin-left: 0.2rem;
scale: 0.9;
color: hsl(var(--text-secondary));
outline: none !important;
}
.chat-logo {
border-radius: var(--radius);
user-select: none;
}
.broadcast-markdown {
* {
font-size: 0.9rem;
color: hsl(var(--text));
}
br {
display: none;
}
}
================================================
FILE: app/src/assets/markdown/all.less
================================================
@import "highlight.less";
@import "style.less";
@import "theme.less";
.file-instance {
display: flex;
flex-direction: column;
margin: 2px 0 !important;
margin-bottom: 4px !important;
padding: 0.5rem !important;
border-radius: var(--radius);
.file-content {
background: none !important;
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
}
img {
max-width: 320px;
max-height: 240px;
object-fit: cover;
border-radius: var(--radius);
margin: 0.25rem 0;
@media (max-width: 768px) {
max-width: 100%;
max-height: 100%;
}
}
.download-action {
background: none !important;
border-radius: 0 !important;
}
@media (max-width: 668px) {
& {
white-space: pre-wrap;
}
}
svg {
width: 0.85rem;
height: 0.85rem;
color: hsl(var(--text));
flex-shrink: 0;
}
.name {
content: attr(file);
display: block;
color: hsl(var(--text));
margin-right: 6px;
font-family: var(--font-family) !important;
}
}
.markdown-body {
pre code {
color: #c9d1d9;
}
pre.file-block div {
background: none !important;
}
pre.file-block,
pre.file-block > div {
padding: 0;
margin: 0;
background: none !important;
box-shadow: none !important;
&:before {
content: none !important;
}
}
pre.file-block > div.file-instance {
background: hsla(var(--background-container)) !important;
}
.markdown-syntax {
position: relative;
.markdown-syntax-header {
position: absolute;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
z-index: 1;
top: -34px;
right: 0;
user-select: none;
cursor: pointer;
transition: 0.25s ease;
&:hover {
p, svg {
color: hsl(var(--text-dark));
}
}
p {
color: hsl(var(--text-secondary-dark));
font-size: 12px;
line-height: 1;
margin: 0 0 0 6px;
padding: 0;
transition: 0.25s ease;
}
svg {
color: hsl(var(--text-secondary-dark));
transition: 0.25s ease;
}
}
}
}
.virtual-prompt {
text-align: center;
border-radius: var(--radius);
border: 1px solid hsl(var(--border));
padding: 0.5rem;
margin-bottom: 0.5rem !important;
}
.virtual-action {
svg {
transform: translateY(1px);
}
}
p:has(.virtual-action) {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
justify-content: end;
margin-top: 1.25rem !important;
br {
display: none;
}
@media (max-width: 768px) {
grid-template-columns: 1fr 1fr;
}
@media (max-width: 480px) {
grid-template-columns: 1fr;
}
}
================================================
FILE: app/src/assets/markdown/highlight.less
================================================
/*!
Theme: GitHub
Description: Light theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Outdated base version: https://github.com/primer/github-syntax-light
Current colors taken from GitHub's CSS
License: MIT
*/
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em
}
code.hljs {
padding: 3px 5px
}
.hljs {
color: #24292e;
background: #fff
}
.hljs-doctag,
.hljs-keyword,
.hljs-meta .hljs-keyword,
.hljs-template-tag,
.hljs-template-variable,
.hljs-type,
.hljs-variable.language_ {
color: #d73a49
}
.hljs-title,
.hljs-title.class_,
.hljs-title.class_.inherited__,
.hljs-title.function_ {
color: #6f42c1
}
.hljs-attr,
.hljs-attribute,
.hljs-literal,
.hljs-meta,
.hljs-number,
.hljs-operator,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-id,
.hljs-variable {
color: #005cc5
}
.hljs-meta .hljs-string,
.hljs-regexp,
.hljs-string {
color: #032f62
}
.hljs-built_in,
.hljs-symbol {
color: #e36209
}
.hljs-code,
.hljs-comment,
.hljs-formula {
color: #6a737d
}
.hljs-name,
.hljs-quote,
.hljs-selector-pseudo,
.hljs-selector-tag {
color: #22863a
}
.hljs-subst {
color: #24292e
}
.hljs-section {
color: #005cc5;
font-weight: 700
}
.hljs-bullet {
color: #735c0f
}
.hljs-emphasis {
color: #24292e;
font-style: italic
}
.hljs-strong {
color: #24292e;
font-weight: 700
}
.hljs-addition {
color: #22863a;
background-color: #f0fff4
}
.hljs-deletion {
color: #b31d28;
background-color: #ffeef0
}
================================================
FILE: app/src/assets/markdown/style.less
================================================
/* This is a theme distributed by `starry-night`.
* It’s based on what GitHub uses on their site.
* See for more info. */
:root {
--color-prettylights-syntax-comment: #6e7781;
--color-prettylights-syntax-constant: #0550ae;
--color-prettylights-syntax-entity: #8250df;
--color-prettylights-syntax-storage-modifier-import: #24292f;
--color-prettylights-syntax-entity-tag: #116329;
--color-prettylights-syntax-keyword: #cf222e;
--color-prettylights-syntax-string: #0a3069;
--color-prettylights-syntax-variable: #953800;
--color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
--color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
--color-prettylights-syntax-invalid-illegal-bg: #82071e;
--color-prettylights-syntax-carriage-return-text: #f6f8fa;
--color-prettylights-syntax-carriage-return-bg: #cf222e;
--color-prettylights-syntax-string-regexp: #116329;
--color-prettylights-syntax-markup-list: #3b2300;
--color-prettylights-syntax-markup-heading: #0550ae;
--color-prettylights-syntax-markup-italic: #24292f;
--color-prettylights-syntax-markup-bold: #24292f;
--color-prettylights-syntax-markup-deleted-text: #82071e;
--color-prettylights-syntax-markup-deleted-bg: #ffebe9;
--color-prettylights-syntax-markup-inserted-text: #116329;
--color-prettylights-syntax-markup-inserted-bg: #dafbe1;
--color-prettylights-syntax-markup-changed-text: #953800;
--color-prettylights-syntax-markup-changed-bg: #ffd8b5;
--color-prettylights-syntax-markup-ignored-text: #eaeef2;
--color-prettylights-syntax-markup-ignored-bg: #0550ae;
--color-prettylights-syntax-meta-diff-range: #8250df;
--color-prettylights-syntax-brackethighlighter-angle: #57606a;
--color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
--color-prettylights-syntax-constant-other-reference-link: #0a3069;
}
.dark {
:root {
--color-prettylights-syntax-comment: #8b949e;
--color-prettylights-syntax-constant: #79c0ff;
--color-prettylights-syntax-entity: #d2a8ff;
--color-prettylights-syntax-storage-modifier-import: #c9d1d9;
--color-prettylights-syntax-entity-tag: #7ee787;
--color-prettylights-syntax-keyword: #ff7b72;
--color-prettylights-syntax-string: #a5d6ff;
--color-prettylights-syntax-variable: #ffa657;
--color-prettylights-syntax-brackethighlighter-unmatched: #f85149;
--color-prettylights-syntax-invalid-illegal-text: #f0f6fc;
--color-prettylights-syntax-invalid-illegal-bg: #8e1519;
--color-prettylights-syntax-carriage-return-text: #f0f6fc;
--color-prettylights-syntax-carriage-return-bg: #b62324;
--color-prettylights-syntax-string-regexp: #7ee787;
--color-prettylights-syntax-markup-list: #f2cc60;
--color-prettylights-syntax-markup-heading: #1f6feb;
--color-prettylights-syntax-markup-italic: #c9d1d9;
--color-prettylights-syntax-markup-bold: #c9d1d9;
--color-prettylights-syntax-markup-deleted-text: #ffdcd7;
--color-prettylights-syntax-markup-deleted-bg: #67060c;
--color-prettylights-syntax-markup-inserted-text: #aff5b4;
--color-prettylights-syntax-markup-inserted-bg: #033a16;
--color-prettylights-syntax-markup-changed-text: #ffdfb6;
--color-prettylights-syntax-markup-changed-bg: #5a1e02;
--color-prettylights-syntax-markup-ignored-text: #c9d1d9;
--color-prettylights-syntax-markup-ignored-bg: #1158c7;
--color-prettylights-syntax-meta-diff-range: #d2a8ff;
--color-prettylights-syntax-brackethighlighter-angle: #8b949e;
--color-prettylights-syntax-sublimelinter-gutter-mark: #484f58;
--color-prettylights-syntax-constant-other-reference-link: #a5d6ff;
}
}
.pl-c {
color: var(--color-prettylights-syntax-comment);
}
.pl-c1,
.pl-s .pl-v {
color: var(--color-prettylights-syntax-constant);
}
.pl-e,
.pl-en {
color: var(--color-prettylights-syntax-entity);
}
.pl-smi,
.pl-s .pl-s1 {
color: var(--color-prettylights-syntax-storage-modifier-import);
}
.pl-ent {
color: var(--color-prettylights-syntax-entity-tag);
}
.pl-k {
color: var(--color-prettylights-syntax-keyword);
}
.pl-s,
.pl-pds,
.pl-s .pl-pse .pl-s1,
.pl-sr,
.pl-sr .pl-cce,
.pl-sr .pl-sre,
.pl-sr .pl-sra {
color: var(--color-prettylights-syntax-string);
}
.pl-v,
.pl-smw {
color: var(--color-prettylights-syntax-variable);
}
.pl-bu {
color: var(--color-prettylights-syntax-brackethighlighter-unmatched);
}
.pl-ii {
color: var(--color-prettylights-syntax-invalid-illegal-text);
background-color: var(--color-prettylights-syntax-invalid-illegal-bg);
}
.pl-c2 {
color: var(--color-prettylights-syntax-carriage-return-text);
background-color: var(--color-prettylights-syntax-carriage-return-bg);
}
.pl-sr .pl-cce {
font-weight: bold;
color: var(--color-prettylights-syntax-string-regexp);
}
.pl-ml {
color: var(--color-prettylights-syntax-markup-list);
}
.pl-mh,
.pl-mh .pl-en,
.pl-ms {
font-weight: bold;
color: var(--color-prettylights-syntax-markup-heading);
}
.pl-mi {
font-style: italic;
color: var(--color-prettylights-syntax-markup-italic);
}
.pl-mb {
font-weight: bold;
color: var(--color-prettylights-syntax-markup-bold);
}
.pl-md {
color: var(--color-prettylights-syntax-markup-deleted-text);
background-color: var(--color-prettylights-syntax-markup-deleted-bg);
}
.pl-mi1 {
color: var(--color-prettylights-syntax-markup-inserted-text);
background-color: var(--color-prettylights-syntax-markup-inserted-bg);
}
.pl-mc {
color: var(--color-prettylights-syntax-markup-changed-text);
background-color: var(--color-prettylights-syntax-markup-changed-bg);
}
.pl-mi2 {
color: var(--color-prettylights-syntax-markup-ignored-text);
background-color: var(--color-prettylights-syntax-markup-ignored-bg);
}
.pl-mdr {
font-weight: bold;
color: var(--color-prettylights-syntax-meta-diff-range);
}
.pl-ba {
color: var(--color-prettylights-syntax-brackethighlighter-angle);
}
.pl-sg {
color: var(--color-prettylights-syntax-sublimelinter-gutter-mark);
}
.pl-corl {
text-decoration: underline;
color: var(--color-prettylights-syntax-constant-other-reference-link);
}
================================================
FILE: app/src/assets/markdown/theme.less
================================================
.markdown-body {
color-scheme: light;
--color-prettylights-syntax-comment: #6e7781;
--color-prettylights-syntax-constant: #0550ae;
--color-prettylights-syntax-entity: #8250df;
--color-prettylights-syntax-storage-modifier-import: #24292f;
--color-prettylights-syntax-entity-tag: #116329;
--color-prettylights-syntax-keyword: #cf222e;
--color-prettylights-syntax-string: #0a3069;
--color-prettylights-syntax-variable: #953800;
--color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
--color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
--color-prettylights-syntax-invalid-illegal-bg: #82071e;
--color-prettylights-syntax-carriage-return-text: #f6f8fa;
--color-prettylights-syntax-carriage-return-bg: #cf222e;
--color-prettylights-syntax-string-regexp: #116329;
--color-prettylights-syntax-markup-list: #3b2300;
--color-prettylights-syntax-markup-heading: #0550ae;
--color-prettylights-syntax-markup-italic: #24292f;
--color-prettylights-syntax-markup-bold: #24292f;
--color-prettylights-syntax-markup-deleted-text: #82071e;
--color-prettylights-syntax-markup-deleted-bg: #ffebe9;
--color-prettylights-syntax-markup-inserted-text: #116329;
--color-prettylights-syntax-markup-inserted-bg: #dafbe1;
--color-prettylights-syntax-markup-changed-text: #953800;
--color-prettylights-syntax-markup-changed-bg: #ffd8b5;
--color-prettylights-syntax-markup-ignored-text: #eaeef2;
--color-prettylights-syntax-markup-ignored-bg: #0550ae;
--color-prettylights-syntax-meta-diff-range: #8250df;
--color-prettylights-syntax-brackethighlighter-angle: #57606a;
--color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
--color-prettylights-syntax-constant-other-reference-link: #0a3069;
--color-fg-default: #24292f;
--color-fg-muted: #57606a;
--color-fg-subtle: #6e7781;
--color-canvas-default: #ffffff;
--color-canvas-subtle: #f6f8fa;
--color-border-default: #d0d7de;
--color-border-muted: hsla(210,18%,87%,1);
--color-neutral-muted: rgba(175,184,193,0.2);
--color-accent-fg: #0969da;
--color-accent-emphasis: #0969da;
--color-attention-subtle: #fff8c5;
--color-danger-fg: #cf222e;
}
.dark {
.markdown-body {
color-scheme: dark;
--color-prettylights-syntax-comment: #8b949e;
--color-prettylights-syntax-constant: #79c0ff;
--color-prettylights-syntax-entity: #d2a8ff;
--color-prettylights-syntax-storage-modifier-import: #c9d1d9;
--color-prettylights-syntax-entity-tag: #7ee787;
--color-prettylights-syntax-keyword: #ff7b72;
--color-prettylights-syntax-string: #a5d6ff;
--color-prettylights-syntax-variable: #ffa657;
--color-prettylights-syntax-brackethighlighter-unmatched: #f85149;
--color-prettylights-syntax-invalid-illegal-text: #f0f6fc;
--color-prettylights-syntax-invalid-illegal-bg: #8e1519;
--color-prettylights-syntax-carriage-return-text: #f0f6fc;
--color-prettylights-syntax-carriage-return-bg: #b62324;
--color-prettylights-syntax-string-regexp: #7ee787;
--color-prettylights-syntax-markup-list: #f2cc60;
--color-prettylights-syntax-markup-heading: #1f6feb;
--color-prettylights-syntax-markup-italic: #c9d1d9;
--color-prettylights-syntax-markup-bold: #c9d1d9;
--color-prettylights-syntax-markup-deleted-text: #ffdcd7;
--color-prettylights-syntax-markup-deleted-bg: #67060c;
--color-prettylights-syntax-markup-inserted-text: #aff5b4;
--color-prettylights-syntax-markup-inserted-bg: #033a16;
--color-prettylights-syntax-markup-changed-text: #ffdfb6;
--color-prettylights-syntax-markup-changed-bg: #5a1e02;
--color-prettylights-syntax-markup-ignored-text: #c9d1d9;
--color-prettylights-syntax-markup-ignored-bg: #1158c7;
--color-prettylights-syntax-meta-diff-range: #d2a8ff;
--color-prettylights-syntax-brackethighlighter-angle: #8b949e;
--color-prettylights-syntax-sublimelinter-gutter-mark: #484f58;
--color-prettylights-syntax-constant-other-reference-link: #a5d6ff;
--color-fg-default: #c9d1d9;
--color-fg-muted: #8b949e;
--color-fg-subtle: #6e7681;
--color-canvas-default: #0d1117;
--color-canvas-subtle: #161b22;
--color-border-default: #30363d;
--color-border-muted: #21262d;
--color-neutral-muted: rgba(110,118,129,0.4);
--color-accent-fg: #58a6ff;
--color-accent-emphasis: #1f6feb;
--color-attention-subtle: rgba(187,128,9,0.15);
--color-danger-fg: #f85149;
}
}
.markdown-body {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
margin: 0;
color: var(--color-fg-default);
background-color: var(--color-canvas-default);
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
}
.markdown-body .octicon {
display: inline-block;
fill: currentColor;
vertical-align: text-bottom;
}
.markdown-body h1:hover .anchor .octicon-link:before,
.markdown-body h2:hover .anchor .octicon-link:before,
.markdown-body h3:hover .anchor .octicon-link:before,
.markdown-body h4:hover .anchor .octicon-link:before,
.markdown-body h5:hover .anchor .octicon-link:before,
.markdown-body h6:hover .anchor .octicon-link:before {
width: 16px;
height: 16px;
content: ' ';
display: inline-block;
background-color: currentColor;
-webkit-mask-image: url("data:image/svg+xml, ");
mask-image: url("data:image/svg+xml, ");
}
.markdown-body details,
.markdown-body figcaption,
.markdown-body figure {
display: block;
}
.markdown-body summary {
display: list-item;
}
.markdown-body [hidden] {
display: none !important;
}
.markdown-body a {
background-color: transparent;
color: var(--color-accent-fg);
text-decoration: none;
}
.markdown-body abbr[title] {
border-bottom: none;
text-decoration: underline dotted;
}
.markdown-body b,
.markdown-body strong {
font-weight: var(--base-text-weight-semibold, 600);
}
.markdown-body dfn {
font-style: italic;
}
.markdown-body h1 {
margin: .67em 0;
font-weight: var(--base-text-weight-semibold, 600);
padding-bottom: .3em;
font-size: 2em;
border-bottom: 1px solid var(--color-border-muted);
}
.markdown-body mark {
background-color: var(--color-attention-subtle);
color: var(--color-fg-default);
}
.markdown-body small {
font-size: 90%;
}
.markdown-body sub,
.markdown-body sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
.markdown-body sub {
bottom: -0.25em;
}
.markdown-body sup {
top: -0.5em;
}
.markdown-body img {
border-style: none;
max-width: 100%;
box-sizing: content-box;
background-color: var(--color-canvas-default);
border-radius: 4px;
max-height: 50vh;
margin: 0.5rem auto;
&.broken {
display: none;
}
}
.markdown-body code,
.markdown-body kbd,
.markdown-body pre,
.markdown-body samp {
font-family: monospace;
font-size: 1em;
}
.markdown-body figure {
margin: 1em 40px;
}
.markdown-body hr {
box-sizing: content-box;
overflow: hidden;
background: transparent;
border-bottom: 1px solid var(--color-border-muted);
height: .25em;
padding: 0;
margin: 24px 0;
background-color: var(--color-border-default);
border: 0;
}
.markdown-body input {
font: inherit;
margin: 0;
overflow: visible;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.markdown-body [type=button],
.markdown-body [type=reset],
.markdown-body [type=submit] {
-webkit-appearance: button;
}
.markdown-body [type=checkbox],
.markdown-body [type=radio] {
box-sizing: border-box;
padding: 0;
}
.markdown-body [type=number]::-webkit-inner-spin-button,
.markdown-body [type=number]::-webkit-outer-spin-button {
height: auto;
}
.markdown-body [type=search]::-webkit-search-cancel-button,
.markdown-body [type=search]::-webkit-search-decoration {
-webkit-appearance: none;
}
.markdown-body ::-webkit-input-placeholder {
color: inherit;
opacity: .54;
}
.markdown-body ::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
.markdown-body a:hover {
text-decoration: underline;
}
.markdown-body ::placeholder {
color: var(--color-fg-subtle);
opacity: 1;
}
.markdown-body hr::before {
display: table;
content: "";
}
.markdown-body hr::after {
display: table;
clear: both;
content: "";
}
.markdown-body table {
border-spacing: 0;
border-collapse: collapse;
display: block;
width: 100%;
overflow: auto;
}
.markdown-body td,
.markdown-body th {
padding: 0;
}
.markdown-body details summary {
cursor: pointer;
}
.markdown-body details:not([open])>*:not(summary) {
display: none !important;
}
.markdown-body a:focus,
.markdown-body [role=button]:focus,
.markdown-body input[type=radio]:focus,
.markdown-body input[type=checkbox]:focus {
outline: 2px solid var(--color-accent-fg);
outline-offset: -2px;
box-shadow: none;
}
.markdown-body a:focus:not(:focus-visible),
.markdown-body [role=button]:focus:not(:focus-visible),
.markdown-body input[type=radio]:focus:not(:focus-visible),
.markdown-body input[type=checkbox]:focus:not(:focus-visible) {
outline: solid 1px transparent;
}
.markdown-body a:focus-visible,
.markdown-body [role=button]:focus-visible,
.markdown-body input[type=radio]:focus-visible,
.markdown-body input[type=checkbox]:focus-visible {
outline: 2px solid var(--color-accent-fg);
outline-offset: -2px;
box-shadow: none;
}
.markdown-body a:not([class]):focus,
.markdown-body a:not([class]):focus-visible,
.markdown-body input[type=radio]:focus,
.markdown-body input[type=radio]:focus-visible,
.markdown-body input[type=checkbox]:focus,
.markdown-body input[type=checkbox]:focus-visible {
outline-offset: 0;
}
.markdown-body kbd {
display: inline-block;
padding: 3px 5px;
font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
line-height: 10px;
color: var(--color-fg-default);
vertical-align: middle;
background-color: var(--color-canvas-subtle);
border: solid 1px var(--color-neutral-muted);
border-bottom-color: var(--color-neutral-muted);
border-radius: 6px;
box-shadow: inset 0 -1px 0 var(--color-neutral-muted);
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: var(--base-text-weight-semibold, 600);
line-height: 1.25;
}
.markdown-body h2 {
font-weight: var(--base-text-weight-semibold, 600);
padding-bottom: .3em;
font-size: 1.5em;
border-bottom: 1px solid var(--color-border-muted);
}
.markdown-body h3 {
font-weight: var(--base-text-weight-semibold, 600);
font-size: 1.25em;
}
.markdown-body h4 {
font-weight: var(--base-text-weight-semibold, 600);
font-size: 1em;
}
.markdown-body h5 {
font-weight: var(--base-text-weight-semibold, 600);
font-size: .875em;
}
.markdown-body h6 {
font-weight: var(--base-text-weight-semibold, 600);
font-size: .85em;
color: var(--color-fg-muted);
}
.markdown-body p {
margin-top: 0;
margin-bottom: 10px;
}
.markdown-body blockquote {
margin: 0;
padding: 0 1em;
color: var(--color-fg-muted);
border-left: .25em solid var(--color-border-default);
}
.markdown-body ul,
.markdown-body ol {
margin-top: 0;
margin-bottom: 0;
padding-left: 2em;
}
.markdown-body ol ol,
.markdown-body ul ol {
list-style-type: lower-roman;
}
.markdown-body ul ul ol,
.markdown-body ul ol ol,
.markdown-body ol ul ol,
.markdown-body ol ol ol {
list-style-type: lower-alpha;
}
.markdown-body dd {
margin-left: 0;
}
.markdown-body tt,
.markdown-body code,
.markdown-body samp {
font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
font-size: 12px;
}
.markdown-body pre {
margin-top: 0;
margin-bottom: 0;
font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
font-size: 12px;
word-wrap: normal;
}
.markdown-body .octicon {
display: inline-block;
overflow: visible !important;
vertical-align: text-bottom;
fill: currentColor;
}
.markdown-body input::-webkit-outer-spin-button,
.markdown-body input::-webkit-inner-spin-button {
margin: 0;
-webkit-appearance: none;
appearance: none;
}
.markdown-body::before {
display: table;
content: "";
}
.markdown-body::after {
display: table;
clear: both;
content: "";
}
.markdown-body>*:first-child {
margin-top: 0 !important;
}
.markdown-body>*:last-child {
margin-bottom: 0 !important;
}
.markdown-body a:not([href]) {
color: inherit;
text-decoration: none;
}
.markdown-body .absent {
color: var(--color-danger-fg);
}
.markdown-body .anchor {
float: left;
padding-right: 4px;
margin-left: -20px;
line-height: 1;
}
.markdown-body .anchor:focus {
outline: none;
}
.markdown-body p,
.markdown-body blockquote,
.markdown-body ul,
.markdown-body ol,
.markdown-body dl,
.markdown-body table,
.markdown-body pre,
.markdown-body details {
margin-top: 0;
margin-bottom: 16px;
}
.markdown-body blockquote>:first-child {
margin-top: 0;
}
.markdown-body blockquote>:last-child {
margin-bottom: 0;
}
.markdown-body h1 .octicon-link,
.markdown-body h2 .octicon-link,
.markdown-body h3 .octicon-link,
.markdown-body h4 .octicon-link,
.markdown-body h5 .octicon-link,
.markdown-body h6 .octicon-link {
color: var(--color-fg-default);
vertical-align: middle;
visibility: hidden;
}
.markdown-body h1:hover .anchor,
.markdown-body h2:hover .anchor,
.markdown-body h3:hover .anchor,
.markdown-body h4:hover .anchor,
.markdown-body h5:hover .anchor,
.markdown-body h6:hover .anchor {
text-decoration: none;
}
.markdown-body h1:hover .anchor .octicon-link,
.markdown-body h2:hover .anchor .octicon-link,
.markdown-body h3:hover .anchor .octicon-link,
.markdown-body h4:hover .anchor .octicon-link,
.markdown-body h5:hover .anchor .octicon-link,
.markdown-body h6:hover .anchor .octicon-link {
visibility: visible;
}
.markdown-body h1 tt,
.markdown-body h1 code,
.markdown-body h2 tt,
.markdown-body h2 code,
.markdown-body h3 tt,
.markdown-body h3 code,
.markdown-body h4 tt,
.markdown-body h4 code,
.markdown-body h5 tt,
.markdown-body h5 code,
.markdown-body h6 tt,
.markdown-body h6 code {
padding: 0 .2em;
font-size: inherit;
}
.markdown-body summary h1,
.markdown-body summary h2,
.markdown-body summary h3,
.markdown-body summary h4,
.markdown-body summary h5,
.markdown-body summary h6 {
display: inline-block;
}
.markdown-body summary h1 .anchor,
.markdown-body summary h2 .anchor,
.markdown-body summary h3 .anchor,
.markdown-body summary h4 .anchor,
.markdown-body summary h5 .anchor,
.markdown-body summary h6 .anchor {
margin-left: -40px;
}
.markdown-body summary h1,
.markdown-body summary h2 {
padding-bottom: 0;
border-bottom: 0;
}
.markdown-body ul.no-list,
.markdown-body ol.no-list {
padding: 0;
list-style-type: none;
}
.markdown-body ol[type=a] {
list-style-type: lower-alpha;
}
.markdown-body ol[type=A] {
list-style-type: upper-alpha;
}
.markdown-body ol[type=i] {
list-style-type: lower-roman;
}
.markdown-body ol[type=I] {
list-style-type: upper-roman;
}
.markdown-body ol[type="1"] {
list-style-type: decimal;
}
.markdown-body div>ol:not([type]) {
list-style-type: decimal;
}
.markdown-body ul ul,
.markdown-body ul ol,
.markdown-body ol ol,
.markdown-body ol ul {
margin-top: 0;
margin-bottom: 0;
}
.markdown-body li>p {
margin-top: 16px;
}
.markdown-body li+li {
margin-top: .25em;
}
.markdown-body dl {
padding: 0;
}
.markdown-body dl dt {
padding: 0;
margin-top: 16px;
font-size: 1em;
font-style: italic;
font-weight: var(--base-text-weight-semibold, 600);
}
.markdown-body dl dd {
padding: 0 16px;
margin-bottom: 16px;
}
.markdown-body table th {
font-weight: var(--base-text-weight-semibold, 600);
}
.markdown-body table th,
.markdown-body table td {
padding: 6px 13px;
border: 1px solid var(--color-border-default);
}
.markdown-body table tr {
background-color: var(--color-canvas-default);
border-top: 1px solid var(--color-border-muted);
}
.markdown-body table tr:nth-child(2n) {
background-color: var(--color-canvas-subtle);
}
.markdown-body table img {
background-color: transparent;
}
.markdown-body img[align=right] {
padding-left: 20px;
}
.markdown-body img[align=left] {
padding-right: 20px;
}
.markdown-body .emoji {
max-width: none;
vertical-align: text-top;
background-color: transparent;
}
.markdown-body span.frame {
display: block;
overflow: hidden;
}
.markdown-body span.frame>span {
display: block;
float: left;
width: auto;
padding: 7px;
margin: 13px 0 0;
overflow: hidden;
border: 1px solid var(--color-border-default);
}
.markdown-body span.frame span img {
display: block;
float: left;
}
.markdown-body span.frame span span {
display: block;
padding: 5px 0 0;
clear: both;
color: var(--color-fg-default);
}
.markdown-body span.align-center {
display: block;
overflow: hidden;
clear: both;
}
.markdown-body span.align-center>span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: center;
}
.markdown-body span.align-center span img {
margin: 0 auto;
text-align: center;
}
.markdown-body span.align-right {
display: block;
overflow: hidden;
clear: both;
}
.markdown-body span.align-right>span {
display: block;
margin: 13px 0 0;
overflow: hidden;
text-align: right;
}
.markdown-body span.align-right span img {
margin: 0;
text-align: right;
}
.markdown-body span.float-left {
display: block;
float: left;
margin-right: 13px;
overflow: hidden;
}
.markdown-body span.float-left span {
margin: 13px 0 0;
}
.markdown-body span.float-right {
display: block;
float: right;
margin-left: 13px;
overflow: hidden;
}
.markdown-body span.float-right>span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: right;
}
.markdown-body code,
.markdown-body tt {
padding: .2em .4em;
margin: 0;
font-size: 85%;
white-space: break-spaces;
background-color: var(--color-neutral-muted);
border-radius: 6px;
}
.markdown-body code br,
.markdown-body tt br {
display: none;
}
.markdown-body del code {
text-decoration: inherit;
}
.markdown-body samp {
font-size: 85%;
}
.markdown-body pre code {
font-size: 100%;
}
.markdown-body pre>code {
padding: 0;
margin: 0;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
.markdown-body .highlight {
margin-bottom: 16px;
}
.markdown-body .highlight pre {
margin-bottom: 0;
word-break: normal;
}
.markdown-body .highlight pre,
.markdown-body pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: var(--color-canvas-subtle);
border-radius: 6px;
}
.markdown-body pre code,
.markdown-body pre tt {
display: inline;
max-width: auto;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
}
.markdown-body .csv-data td,
.markdown-body .csv-data th {
padding: 5px;
overflow: hidden;
font-size: 12px;
line-height: 1;
text-align: left;
white-space: nowrap;
}
.markdown-body .csv-data .blob-num {
padding: 10px 8px 9px;
text-align: right;
background: var(--color-canvas-default);
border: 0;
}
.markdown-body .csv-data tr {
border-top: 0;
}
.markdown-body .csv-data th {
font-weight: var(--base-text-weight-semibold, 600);
background: var(--color-canvas-subtle);
border-top: 0;
}
.markdown-body [data-footnote-ref]::before {
content: "[";
}
.markdown-body [data-footnote-ref]::after {
content: "]";
}
.markdown-body .footnotes {
font-size: 12px;
color: var(--color-fg-muted);
border-top: 1px solid var(--color-border-default);
}
.markdown-body .footnotes ol {
padding-left: 16px;
}
.markdown-body .footnotes ol ul {
display: inline-block;
padding-left: 16px;
margin-top: 16px;
}
.markdown-body .footnotes li {
position: relative;
}
.markdown-body .footnotes li:target::before {
position: absolute;
top: -8px;
right: -8px;
bottom: -8px;
left: -24px;
pointer-events: none;
content: "";
border: 2px solid var(--color-accent-emphasis);
border-radius: 6px;
}
.markdown-body .footnotes li:target {
color: var(--color-fg-default);
}
.markdown-body .footnotes .data-footnote-backref g-emoji {
font-family: monospace;
}
.markdown-body .pl-c {
color: var(--color-prettylights-syntax-comment);
}
.markdown-body .pl-c1,
.markdown-body .pl-s .pl-v {
color: var(--color-prettylights-syntax-constant);
}
.markdown-body .pl-e,
.markdown-body .pl-en {
color: var(--color-prettylights-syntax-entity);
}
.markdown-body .pl-smi,
.markdown-body .pl-s .pl-s1 {
color: var(--color-prettylights-syntax-storage-modifier-import);
}
.markdown-body .pl-ent {
color: var(--color-prettylights-syntax-entity-tag);
}
.markdown-body .pl-k {
color: var(--color-prettylights-syntax-keyword);
}
.markdown-body .pl-s,
.markdown-body .pl-pds,
.markdown-body .pl-s .pl-pse .pl-s1,
.markdown-body .pl-sr,
.markdown-body .pl-sr .pl-cce,
.markdown-body .pl-sr .pl-sre,
.markdown-body .pl-sr .pl-sra {
color: var(--color-prettylights-syntax-string);
}
.markdown-body .pl-v,
.markdown-body .pl-smw {
color: var(--color-prettylights-syntax-variable);
}
.markdown-body .pl-bu {
color: var(--color-prettylights-syntax-brackethighlighter-unmatched);
}
.markdown-body .pl-ii {
color: var(--color-prettylights-syntax-invalid-illegal-text);
background-color: var(--color-prettylights-syntax-invalid-illegal-bg);
}
.markdown-body .pl-c2 {
color: var(--color-prettylights-syntax-carriage-return-text);
background-color: var(--color-prettylights-syntax-carriage-return-bg);
}
.markdown-body .pl-sr .pl-cce {
font-weight: bold;
color: var(--color-prettylights-syntax-string-regexp);
}
.markdown-body .pl-ml {
color: var(--color-prettylights-syntax-markup-list);
}
.markdown-body .pl-mh,
.markdown-body .pl-mh .pl-en,
.markdown-body .pl-ms {
font-weight: bold;
color: var(--color-prettylights-syntax-markup-heading);
}
.markdown-body .pl-mi {
font-style: italic;
color: var(--color-prettylights-syntax-markup-italic);
}
.markdown-body .pl-mb {
font-weight: bold;
color: var(--color-prettylights-syntax-markup-bold);
}
.markdown-body .pl-md {
color: var(--color-prettylights-syntax-markup-deleted-text);
background-color: var(--color-prettylights-syntax-markup-deleted-bg);
}
.markdown-body .pl-mi1 {
color: var(--color-prettylights-syntax-markup-inserted-text);
background-color: var(--color-prettylights-syntax-markup-inserted-bg);
}
.markdown-body .pl-mc {
color: var(--color-prettylights-syntax-markup-changed-text);
background-color: var(--color-prettylights-syntax-markup-changed-bg);
}
.markdown-body .pl-mi2 {
color: var(--color-prettylights-syntax-markup-ignored-text);
background-color: var(--color-prettylights-syntax-markup-ignored-bg);
}
.markdown-body .pl-mdr {
font-weight: bold;
color: var(--color-prettylights-syntax-meta-diff-range);
}
.markdown-body .pl-ba {
color: var(--color-prettylights-syntax-brackethighlighter-angle);
}
.markdown-body .pl-sg {
color: var(--color-prettylights-syntax-sublimelinter-gutter-mark);
}
.markdown-body .pl-corl {
text-decoration: underline;
color: var(--color-prettylights-syntax-constant-other-reference-link);
}
.markdown-body g-emoji {
display: inline-block;
min-width: 1ch;
font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
font-size: 1em;
font-style: normal !important;
font-weight: var(--base-text-weight-normal, 400);
line-height: 1;
vertical-align: -0.075em;
}
.markdown-body g-emoji img {
width: 1em;
height: 1em;
}
.markdown-body .task-list-item {
list-style-type: none;
}
.markdown-body .task-list-item label {
font-weight: var(--base-text-weight-normal, 400);
}
.markdown-body .task-list-item.enabled label {
cursor: pointer;
}
.markdown-body .task-list-item+.task-list-item {
margin-top: 4px;
}
.markdown-body .task-list-item .handle {
display: none;
}
.markdown-body .task-list-item-checkbox {
margin: 0 .2em .25em -1.4em;
vertical-align: middle;
}
.markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox {
margin: 0 -1.6em .25em .2em;
}
.markdown-body .contains-task-list {
position: relative;
}
.markdown-body .contains-task-list:hover .task-list-item-convert-container,
.markdown-body .contains-task-list:focus-within .task-list-item-convert-container {
display: block;
width: auto;
height: 24px;
overflow: visible;
clip: auto;
}
.markdown-body ::-webkit-calendar-picker-indicator {
filter: invert(50%);
}
================================================
FILE: app/src/assets/pages/api.less
================================================
.api-dialog {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: max-content;
padding: 24px 0 12px;
gap: 24px;
}
.api-wrapper {
display: flex;
flex-direction: row;
gap: 6px;
width: 100%;
input {
text-align: center;
font-size: 16px;
cursor: pointer;
flex-grow: 1;
}
button {
flex-shrink: 0;
}
}
================================================
FILE: app/src/assets/pages/article.less
================================================
.article-page {
position: relative;
display: flex;
width: 100%;
min-height: calc(100% - var(--navbar-height));
height: max-content;
}
.article-container {
display: flex;
flex-direction: column;
padding: 12px 16px;
gap: 6px;
width: 100%;
height: 100%;
}
.article-wrapper {
width: calc(96vw - 32px);
height: 100%;
margin: 1rem auto;
padding: 1rem;
max-width: 840px;
.article-title {
display: flex;
flex-direction: row;
user-select: none;
align-items: center;
}
.article-action {
@media (max-width: 768px) {
flex-direction: column;
}
}
.article-content {
display: flex;
flex-direction: column;
margin: 1rem 0;
& > * {
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
}
}
}
================================================
FILE: app/src/assets/pages/auth.less
================================================
.auth {
width: 100%;
height: 100%;
overflow: hidden;
}
.auth-container {
display: flex;
flex-direction: column;
align-items: center;
justify-items: center;
padding: 2.5rem 2rem;
height: fit-content;
user-select: none;
.logo {
width: 4rem;
height: 4rem;
border-radius: var(--radius);
}
& > * {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
}
}
.auth-card {
width: 80vw;
max-width: 360px;
min-width: 280px;
margin: 1rem 0;
}
.auth-wrapper {
display: flex;
flex-direction: column;
padding: 1.5rem 0;
& > * {
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
}
}
.addition-wrapper {
display: flex;
flex-direction: column;
border-radius: var(--radius);
border: 1px solid hsla(var(--border));
padding: 1.25rem;
align-items: center;
transform: translateY(-1rem);
font-size: 0.875rem;
text-align: center;
a {
text-decoration: underline;
text-underline-offset: 0.25rem;
text-underline: 2px solid hsl(var(--text-secondary));
color: hsl(var(--text-secondary));
transition: 0.2s ease-in-out;
cursor: pointer;
&:hover {
color: hsl(var(--text));
text-underline-color: hsl(var(--text));
}
}
.row {
margin-bottom: 0.5rem;
.link {
margin-left: 0.1rem;
}
&:last-child {
margin-bottom: 0;
}
}
}
================================================
FILE: app/src/assets/pages/chat.less
================================================
@keyframes FlexInAnimationFromBottom {
0% {
opacity: .2;
margin-top: 20px;
margin-bottom: 0;
}
100% {
opacity: 1;
margin-top: 0;
margin-bottom: 20px;
}
}
.scroll-action {
position: absolute;
z-index: 12;
opacity: 0;
right: 36px;
bottom: 12rem;
transition: .25s;
pointer-events: none;
button {
border-color: rgba(0,0,0,0) !important;
}
&.active {
opacity: 0.8;
pointer-events: all;
}
@media (max-width: 668px) {
bottom: 8.5rem;
}
}
.message {
display: flex;
gap: 6px;
flex-direction: column;
max-width: 100%;
pre {
scrollbar-width: thin;
&::-webkit-scrollbar {
height: 6px;
}
}
&:last-child {
animation: FlexInAnimationFromBottom 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0s 1 normal forwards running;
.bing {
animation: fadein 0.2s ease-in-out;
@keyframes fadein {
from { opacity: 0.5; }
to { opacity: 1; }
}
}
}
.content-wrapper {
display: flex;
flex-direction: row;
max-width: 100%;
.message-toolbar {
display: flex;
flex-direction: column;
padding: 0 4px;
user-select: none;
height: max-content;
margin-top: auto;
gap: 4px;
svg {
cursor: pointer;
color: hsl(var(--text-secondary));
transition: 0.25s;
&:hover {
color: hsl(var(--text));
}
}
}
}
.message-quota {
display: flex;
flex-direction: row;
align-items: center;
user-select: none;
gap: 4px;
cursor: pointer;
border: 1px solid hsl(var(--input));
border-radius: var(--radius);
transition: 0.2s linear;
padding: 4px 8px;
width: max-content;
height: max-content;
white-space: nowrap;
margin-left: 3rem;
transition-property: border-color, color, background-color, width;
&.subscription {
svg, span {
color: hsl(var(--gold));
}
}
.quota {
font-size: 14px;
color: hsl(var(--text-secondary));
transition: .25s;
}
.icon {
color: hsl(var(--text-secondary));
}
&:hover {
border-color: hsl(var(--border-hover));
}
}
.message-content {
display: flex;
flex: 1 1 auto;
min-width: 0;
flex-direction: column;
max-width: 100%;
padding: 8px 16px;
border-radius: var(--radius);
transition: 0.25s linear;
}
.message-avatar-wrapper {
.message-avatar {
display: flex;
width: 2.25rem;
height: 2.25rem;
border-radius: var(--radius);
text-align: center;
font-size: 0.785rem;
}
flex-shrink: 0;
margin-left: 0.5rem;
border-radius: var(--radius);
border: 1px solid hsl(var(--border));
width: 2.25rem;
height: 2.25rem;
user-select: none;
}
&.user {
align-items: flex-end;
}
@media (max-width: 768px) {
.content-wrapper {
flex-direction: column !important;
align-items: flex-start;
.message-avatar-wrapper {
margin-left: 0;
margin-right: 0;
margin-bottom: 0.5rem;
}
}
.message-toolbar {
padding: 0 !important;
}
.message-quota {
margin-left: 0;
}
&.user {
.content-wrapper {
align-items: flex-end;
}
}
}
&.user {
.content-wrapper {
flex-direction: row-reverse;
}
}
&.assistant, &.system {
align-items: flex-start;
.message-avatar-wrapper {
margin-right: 0.5rem;
margin-left: 0;
}
}
}
.markdown-body {
max-width: 100%;
padding: 4px 0;
background: none !important;
color: hsl(var(--text));
.prompt-row {
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
margin: 0.25rem 0;
white-space: nowrap;
.grow {
min-width: 0.75rem;
}
.value {
display: flex;
flex-direction: row;
align-items: center;
font-family: var(--font-family);
margin: 0 !important;
}
svg {
transform: translateY(1px);
}
}
ol, ul, menu {
list-style: inherit;
}
pre, code {
font-family: var(--font-family-code) !important;
background: rgb(40, 44, 52);
}
pre {
// width: calc(100% - 8px);
box-shadow: #0005 0 2px 2px;
border-radius: var(--radius);
&:before {
content: "";
display: block;
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAcIAAACCCAYAAADVN8idAAAgAElEQVR4nO2de5QU5Zn/v1VdVX2/zQwMzDCDgCBKOIx4myXLRlnYGDlhzWWDSTxkhXBQo2iS34kmavb3C5qo5+yqqBs5xNG4ZpVskjXk6BrhqAkbdoyXgSUoiqgMzDjAzPS1+lLX3x/TYNU7F6C7untm+vn8Ne/bVdVvP+8777fe2/MABEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQExKu2BtN03SyHGVhxdS61jk+77xWr3dWk9c7Y4okTakThbqAIIa8POcTeF4EAM0w1KxhZtKamhxUtcETinKiN5s92p3Nfngok31vx/HB7mr/FmLisaItMGv2NPfclqnCrKYGoXVqWJxWF+TrAj4u5JE4n+jiRZMzoWmmmlPMTDpjJgdTxuDxhNrX2691HzmuffhBX/7gjj3pD6v9W4iJx9TFwXqxWWrlG6UmforYiIhQb4ZcEcPPBzjJ5eZd4AHA0GGYip7nZSPNJfU44tqAcUI9ZhxTetUepfv4W6mBav+W08FxRUvZ0P3F3jjehHBByM+3RyNLLw6H29vCwQubPJ6ZhY/aS3x0JwD05nKH9yRSXW8kEp2dsfiu/UnZKPG5xCRiQYuHb5/vvfyieZ4lbXO8FzU1uE62vwtLfHQXAPT064f3Hsq++cZ7ud2vHci+uv9IjtofYWP6VfWfEud7F2Gu9wJMEacVsteW+NgOAMAJtQ8Hs2+rB7J7P35h4C8lPtNxaloI2+tDkRUNDSuvqG9YPsfvnY/SRe9M6TwkywdeGRjcuaO///nOgWS8Qt9LjCP+ap6v/m8X+1de3ua78twmaT5KF70zpev9XuXAq3syL+54S97+2nsZan81SMN8v9tzaXApvziwBDOkky9epQrf6RgSxqPKYeOt9O7cn1O7+g/I+TJ/52mpSSG8aXbL51ZNa/zCeX7/QlRO/Eaj811Z3re979h/PvLBkf+qclmICvCtlfUrP78k8JX5LdJCVE78RqPrwBFl3+92p3/56PMDz1e5LEQFaPrClEvFvw4uN2d65qD8wnc6OozDuUPmrtTve5478Wa1ClEzQtgW8bu/1ty8dnXT9DWFrDEFkOcAURIhCiJEQYDL5YLocoEXePDgwPHcKeOZpgnTMGHAhKEZUHUduq5D1TSomgo1r+IM5qE6TcD4Ze/HT//70Z6OPYnqvyURzrHoHK/7a8tCG1ZfHvrHQtaYAsgBkCQXRJGDKLggCIDg4uHiOfCFtsdxQ/9DpsnBNE0YhgndMKHpBjQNUDUdqmpCUXScwX9bl2lyxrZXk08+80pi696PstT+JhnN1zdf7Voe/nwhOaYAGpwJSRTBSQK4ocYH3sXD5eIB3gWe42AU2h9vcjBMEzB06LoBQzcATYepaTAVDYqqgjdPKxUdnAld3xl/4eiW3udK/7Vnx6QXwraI372uteWmVY2NX8ZpxM/r8cDjluCRJEhu0dFyKHkVOUVBLq8gm8ud7vLO7ceO/erxw0ceIUGc2Cw6x+ted2Xk1lVLAqtxGvHzelzwuAV43DzcEu9oOfKKgVzeQC6vIZvTT3d51/bd6W2Pvxh/kARx4tNyy4zV3NLQ3xWSowqg4HGD94jg3SJcDvd/el6FkVdh5FRouTGbVAcAmLuSLx156Og2RwsxBpNaCH98/rnrvz6jeS3GEECf1wuf1wOf112yMc4U0zSRyeaRyeaQyWbHurTzF0d7On7wzvtbK1IwwlHuWdN4w9eXh9ZjDAH0eUX4vTx8XlfF2p9hmshmdchZDZnsmKLY9fTO5JY7nzq2pSIFIxyl5ZvTV3JXRr9YSI4ogJzPA7fXDc4nga9g+zMzCvLZPMzMqIOCIUF8MfabIz/7uOxT9pNSCNfNbP7MrbNn3RYSXFGMIIKiICLg98Lv98LFO/vmfbbohgFZziItZ6Fq6kiXdCY1LfbgBx/d9/jhnj9UunzE2bNuRXTZLV+quyPk46MYQQRFgUfALyLgF+CqbvODrgPpjIa0rELVRpzA70pmjNiDvx68p2NH7OVKl484e6ZfVf8p1zUN63mfK4CRBFBwQQp44fJ7wFe5ARq6AV3OQUlnAW3El7IOyEZa3XZiazl3m04qIVwQ8vPfnzt709K6umUYQQDdbhFBfwB+n8fx73YCOZNDSk4jnx9ZEHcNDr78k4Mf3EVHL8YnC1o8/G3X1N/7Nwt9yzGCALolF0IBAX6fUIXSnR45oyGZ1pBXRuyQuv74v9md923rv52OXoxfWu+cuQFt/ksxggAKkgQh6IXL765CyU6PLuehpWRoijbSxx3YI/+5++7DZZmdmDRC+I3WpiU/nDvnXoHnl7KfSYKAUCg4bgWQRc7kkEymoGjDG4RqGH/YdPDQD37e3bu7CkUjRmHNsujSO6+tv18SuOEzECKHSFAatwLIImc0xFMKVHX4/6iimZ13Pz3wvadeju2qQtGIUZj+2boLhOumbeQEiGBEkBdFSCHfuBVAFl3OQ0lmYKjDBgQdpmaoesfxB3tfGjzg5HdOCiG8f8G8Gwq7QW2dEM8B4VAIoaDfse+qJMmUjFgiOdJHndt6P37qe/vf+2mly0QM5761jRsLu0Fto0COMxEJeRAOTgwBZEmkNMQT+ZF2nHY9+0qq4/Yn+h6pfKkIFstuUJsAGpwJTzgIMeSrUslKQ01mkEukRtpx2mHsiP/Oyd2lE14If31J2wMXR8LtYETQ5/UiGglCcLkc+Z5qoak6YsnUSJtqOt+IJzq/9Pqeb1ejXMQQv7qj5eGLz/N8GowI+rwi6sIiBKEyGxDKhaaZGEzkR9pU0/X6u/k//cM93TdXo1zEEK2bZm3E+d5FYESQ83ngi/gBYWL3f9B0ZOLySJtqOvBOdm/3XR9uduJrJqwQtteHIg9ccP5jBVdoNhGsi4QRDEzMt6DRSKUzGIwn2OzO3lzu8Lfffud68k5TWS6b54s8cP20Jwqu0GwiWB+REAw4u/282qTSKgbiCpvd1dOvH/7OY33XkXeaytIw3+/23dL0w4IrNJsIeqJBuILeKpWsPOipLHKxFJvdwR1X++TNvT8q1TvNhBTCFVPrWh9euOBxL88vt+a7RQnRaAhuaXJ1QifJKyoG40koir1DyhrGzpv37V9Hzr0rw4q2wKyHvjXt5z43Z1uPlkQXGqISJIfPAI4XFMVAf0yBotpHh5m8uWvjo33X7tyTpvZXAaYuDtZ7vtu8CW7+BtsHkgBfXQicNDGn4k+HqWjIDCYBdjNN3vhp7p977irFufeEE8KrpjXM+deFC57igCXWfL/Pg/popGJnsaqFaZoYiMUhM1MFJrD7xn3717zQ13+oSkWrCT53UWjuo7c0PsMDF1nz/T4BDVHPKW8vkxXT5NAfy0HOaGz+mzdu7v3qf72ZPlilotUEjZeEp7hva74XzChQ9HngqQ+f8vYyWeFNE7mBFFSm/+NMbM3e33PH8dcTJ4p57oQSwhVT61q3Llr4DCuCoWAA0XCw2KJMSGKJFJKptC3PBHav37vvqzQyLA8r2gKztnxn+n+wIhgKCqgLT4wdeU4xmMgjmRouhusf6P0ijQzLw9TFwXrPD1ruByuCIS+kSG31f1osjXwqY8vjTGzN/uTI94sZGZYqhBWbA2qvD0UeXrjgcVYEI6FgzYkgAETDQURC9t/NAUseXrjg8fb6UKRKxZq0XDbPF3noW9N+zopgNCTVnAgCQF3YjUhIsuVxnHnR5m9Ne/qyeT5qfw7TMN/v9ny3eRMYEXSHAzUnggAgRANwhwO2PJPDes93mzc1zK/8OZGKCeEDF5z/GLsmGA2HEA4FRrtl0hMOBRANh2x5Xp5f/sAF5z9WpSJNWh64ftoT7JpgNOxGODQ516PPhEhIRJR5CfC5uaX/cv20J6pUpEmL75amH7JrglIkACE8uTYFng1C2AcpwvT/bv4G/8amH1a6LBURwl9f0vaAJVAugKGR4EQ9H+gkoaB/2MiwyeOZ+etL2h6oUpEmHb+6o+VhS6BcAEMjwYl6PtBJwkFh2MiwucE18z/uaH24SkWadLRumrXREigXwJAITtTzgU4ihnzDR4ZTxWmtm2ZtrGQ5yi6E9y+YdwN7TjAUDNT0SJAlHAogFLTZo/3iSLj9/gXzbhjtHuLMuG9t40b2nGA4KNT0SJAlEhIRsr8UXHjJee5P33vdtJuqVabJQvP1zVez5wTFkJ9E0IIQ9sEdtNljLc73LpqxoenqSpWhrEL4jdamJazHGL/PU5NrgqcjGh7mQq59ddP0Nd9obVoy2j3E2KxZFl3Keozx+4Rh04HE0Joh40LuwmuuCK5dsyw6zOUhcWZM/2zdBazHGNHngRShmTAWIRqAaO//1vIrIp+f/tm6Cyrx/WXbNbog5Oe3X7L4VavvULcooXFq3aQ/IlEspmmi78Sg7Zyhahh/+PvX31pGjrrPjgUtHv4//9+MP1l9h0qiC9Oneif9EYliMU0OHx/P2s4ZKprZ+YV/OvppctR99rQ8e8FjnIANpzIkAcHG+kl/RKJYeNNE6ljMds7Q1IwtR645cP3p7h23u0a/P3f2JtaBdjQaIhEcA47jUBexb54Ref4z3587e1OVijRhue2a+ntZB9oNUYlEcAw4zkRD1L5eKAlc+23X1N9bpSJNWFrvnLmh4ED7FL66EIngGBgcB1+dvf/jBF5svXPmhlFucYyyCOG6mc2fKYRSOkVdJDxpPcY4iVsSURcJ2/KW1tUtWzez+TNVKtKEY92K6LJCKKVT1Ecmr8cYJ5EkHvURuxj+zULf8rUrostGuYVgmH5V/afYUEqeaHDSeoxxEk4S4Inals7Wos1/6fSr6j9Vzu8tS89w6+xZt8GyLujzeied79ByEgz44PPafA223zr7nNuqVZ6Jxi1fqrsDlnVBn1ecdL5Dy0kwIMLntTl7vvDWIZsSZ4Drmob1sIgg5/NMOt+h5cQV9IJj1gvF1VPWl/M7HRfCH59/7vpCZPmhL+CAaA0eGC2VKHOkIiQI0R+ff25ZG8Nk4J41jTcUIssDGJruqwuTCJ4tdWG3bQNByMdH717TWPYpqolOyzenryxElgcwFErJR5tjzhpfxG+fRvbzgZZvTl9Zru9zVAjbIn7312c0r4VlNBgOhSZ8KKVqIIgu9rB9+9dnNK9tC0+Q6JxVYNE5XvfXl4fWwzIajIQ8Ez6UUjUQBA4R++7aC69dHtqw6Bwvtb8x4K6MfhHWKdFwcOKHUqoGgmvIdp+wtmDbsuCoEK5rbbkJFhGUBIEOzZdAKOiHJNjWFdrXzWyhs12jsO7KyK2wiKAocnRovgTCQQGiaHuJuLBgY2IEWm6Zsdqa5kWRzguWgBjygRftszmsjZ3CMSFsi/jdqxobv2zNC4VoSrRUQozjgVWNjV+mUeFwFp3jda9aErD9k0SC0miXE2cIa8NVSwKraVQ4MtzS0N/BMhqUSARLhrHh2oKNHccxIfxas31K1O0W2QPiRBH4fV64RVtn1P61oelnwsLXloU2wDIadEsu9oA4UQR+nwC3ZN8489UrwrRWzdB8fbPNC4ogSXDR+2rJuPxuCMxu23J4nHFMCAseZE4R9JMLNacI2t0P4StN06+tUlHGLQUPMqcIBUgEnYK1JWtrAmA9yAi0S9QxBPvy2lrX8shVTn+HI0J40+yWz1nTokCjQSfx+7wQhU/myjmAZ21ey3xrZb1tN5ko8DQadBC/T4AofNJVcJzJ38jYvJZp+sKUS20ZgotGgw7i8rshWDYcmRxczVdPuWiMW84aR4Rw1bTGL8AyLRrw09uQ0zA2bS/YnADw+SWBr8AyLRrw03EJp2FseuGqIZsTAMS/Di6HdW0wQP2f0/B2m67llgY/6+jzS31Ae30ocp7fv9Ca5ychdBzWpuf5/QspgC/wV/N89fNbJFv7C/hpNOg0AWaEPb9FWkgBfIcC7pozPXOseS4/zYY5DWtTfqZnjpMBfEsWwhUNDSvBeJFx8eTKymlcPD/M20zB9jXN3y72rwTjRcZFzc9xXC4M8zazYrF/VbXKM17wXBpcCsaLDE8N0HF4Fz/M20zB9s48v9QHXFHfYPPp6PPS21C58DG71q+or1s+yqU1w+Vtviutab+XOqFy4ffaR4Ws7WsRfnHAFibNTSdLygZrW9b2pVBSr7Eg5Ofn+L3zrXlsZ004B/uSMcfvn78g5K/Znn9Bi4c/t0li2h958SgXXsa25zZJ8xe0eGq2/QEAZkgzrUnOR2dXy8Uw2zK2L4WSGnF7NLIUlmlRr8dDYZbKCMdx8HrswXsLdVCTtM/3Xg7LtKjX46L2V0Z4joPXY58evWyoDmoSNiKC4HGDp/ZXNniOg+CxD7ScikpRkhBeHA7b4r153PQ2VG5YG7N1UEtcNM9jmxrxuGmTTLlhbXwxUwe1hDjfuwiW9UHeQ7uVyw1j47WFOij9uaXc3BYOXmhNeyQSwnLD2pitg1qibY7XdpbI467tWbpKwNp4EVMHNcVc7wXWJO8mISw3w2zM1EHRzy3l5iaP59QcLc8BEjWEssPa2FoHtUZTg+vUb+c4E24KvFt23BJvC8/UbKmDmmOKOO3knwZnwkX9X9lxuUV7eCZLHZRC0T3Hiql1rda0SNHnK4ab+Ydj66IWWNEWmGVNSyJNi1YKye57dFhd1AJTFwfrrWlJpP6vUrC2ZuuiGIoWwjk+7zxYNspYXYAR5YWxdXuhLmqK2dPcc8GEXCIqAxuaqVAXNYXYLLXCen5QohexSuESmXXCobooiaKFsNXrtb0FigI1hErB2pqti1qgZarAtD86NlEpWFuzdVEL8I1SkzXNUf9XMUzR3v7YuiiGooWwyeudYU27KAp9xWBtzdZFLdDUINjeAqkfqhysrZvqxZqbmueniI22DHoRqxyMrYfVRREULYRTJGmKNS2SEFYM1tZsXdQCU8P2RXKB3FpVDNbWUyO8IxsWJhQRwbYuRW7VKscwWzN1UdQzi72xThTqbA8SqCFUCp7x5crWRS1QF+Rtv9nF0xphpWBtHQ3WXvszQy6bw3EXCWHFYG3N1kUxFF17AUEM2R9EHVGl4JmOiK2LWiDg4+ztj4SwYrC2DjJ1UQsYft4eeZynGbGKwdh6WF0U88hib/TynC1sOkcdUcVgbc3WRS3gkZj2R66tKgZra7YuagFOctl8fZFrtcrBW88RYnhdFPXMYm8UeN62h5U6osrB2pqti1pAdLHtzxztUsJhWFsLAldz7Y932ftOg9pfxTCY/o+ti2KgiW1iQmJSx0MQhEMULYSaYajWtGlSx1QpWFuzdVELaJrJtD+akagUrK3ZuqgFDB2GNc1T+6sYPNP/sXVR1DOLvTFrmBlr2jRICCsFa+usYWRGuXTSklOY9kcvYhWDtTVbF7WAqeh5a9qg9lcxDOalg8sb+VEuPWOKFsK0piataYOEsGKwtk5rWnKUSyct6YxJ7a9KsLZOMXVRC/CykbZlGHqVSlKDsLbO6OmRLzxzihbCQVUbtKYNo+TRKXGGsLZm66IWGEwZtt+skxBWDNbWsVTttT8uqcetaV2n/q9SsLZm66IYihbCE4pywppWdXojqhSsrU/k7XVRCxyPa33WtEYdUcVgbX08bvSNcunkJa4NWJMGtb+KMczWTF0UQ9FC2JvNHrWmdRLCisHaujdnr4taoHdA7bamNa1aJak9WFuzdVELGCfUY7YMjfq/isHYelhdFEHRQtidzX5oTavUE1UM1tZsXdQCR45rTPujjqhSsLbuZuqiFjCOKb3WtEn9X8Vgbc3WRTEULYSHMtn3AHSeTKtaze2grhqqYmsInYW6qCk+6MsfBNB1Mq2qtEZYKRhbd304VBc1hdqjdAPoOJk2FRLCSsHYuqNQFyVRtBDuOD5o+3JVISGsFHlVsaXZuqgFduxJ20YhikIjwkrB2pqti1rg+Fsp27qUolL/VylYW7N1UQwleZbpzeUOn/zbMAElT42h3OQYG1vroNbo6ddP/XYTQF6hDQvlJq8YsI4HrXVQc5xQT20S4k0OOvV/ZUfPqzbnBdxx1ZGNWiUJ4Z5EqsuazinKaJcSDqEwNmbroJbYeyj7pjWdy5MQlptc3j4FyNZBTXEw+7Y1aZAQlh3Wxub79joolpKE8I1EotOazuVJCMsNa2O2DmqJN97L7bam2U6acB72ZYOtg1pCPZDdC8s6oZEjISw3jI07CnVQMiUJYWcsvguWDTPZXI5cXZUR0zSRzeWsWZ2FOqhJXjuQfRWWDTPZnE6ursqIYZrI5mzrg12FOqhJPn5h4C/WtJbLU/srJ8aQja2wdVAsJQnh/qRsHJLlA9a8TLZkt2/EKGSyNhHEIVk+sD8p1+x84P4jOeP9XsXW/rJZ2jRTLljbvt+rHNh/JFez7Q8AcFSxrZGaGZoVKxc6qy2M7Uuh5DBMrwwM7rSm2c6acA72JYO1fS3y6p7Mi9a0nKXp0XLB2pa1fS1ivJW2TQ3naSBQNljbmm+mHZuWL1kId/T3Pw/L9Ggmm4VOfkcdRzcMZLJZa1bnjhP9z1erPOOFHW/J22GZHs1kdZCTI+fR9SHbWugq2L6myf05tQvW84SZHLlbKwOGbsDM2AZZHdnXk44tC5UshJ0Dyfi7srzPmifL2dEuJ4okzdj0XVne1zmYLNnZ7ETntfcy8QNHFFv7S2doVOg0aWbK70C3uu+19zI13/76D8h543DukDVPl2lWzGmG2fSj3KH+AxnHht+ORKjf3nfsP2EZFbKdNlE6sixbk52/7Tv2q2qVZbzxu93pX8IyKkzLtHvPadKyfTS4/X9Sv6xWWcYb5q7U72EZFSpp6v+chrFph/7fyd87+XxHhPCRD478l4lPogSrmgo5Q29FTiFnsjb/jiZgPPrBkZeqWKRxxaPPDzxvmpyl/RmQaVToGHJGg6p9Mt1nmpzxr88P1Py0/El6njvxJmfik39QTYcu01qhU+hy3uZomzOh9zzX7+j5VUeEEAB+2fvx09Z0Si45ViJRIJWyBwDf1tv7VJWKMm7Z9mrySWs6mSYhdArWlqytCUDfGX8BllGhlpLHuJo4GxhbdhRs7SiOCeG/H+3pgGV6NJ+nUaETyJks61u085mjvU9WqTjjlmdeSWyFZXo0r+g0KnQAOaMhb/ct2vXMK/Gt1SrPeOXolt7nrGlN0WhU6AC6nIfGODRnbe0EjgnhnoSc337Mvm6VTKacenzNkkzaR9bbjx371Z4E/Yex7P0om9++O73NmhdP0ZmuUmFt+Nvd6Wf2fpSj9jcC5q7kS7CuFSYzY1xNnAmMDTsKNnYcx4QQAB4/fOQRWEaFiqYhSVMERZNIyVDssbc6CzYmRuDxF+MPggnNlEjRqLBYEillWMiljhdjm6tVnvHOkYeO2l7EDFWFSmJYNGoyA4OJNMHa2CkcFcI9CTn/C2aKNJFIQlPpYNfZoqk6komkNavzF0d7Omg0ODp7P8rmn96Z3AKLGMaTOWgaub06WzTNRDxh64S6nt6Z3EKjwbExX4z9BpZRYS6Rouj1xaDpQ7b7hI6CbcuCo0IIAD945/2tSU2LnUwbAGI0RXrWxJIpWI/lJjUt9oN33qe1mdNw51PHtiQzxqn2Z5ocBhN0nOJsGUzkbeGWErIZu/OpY1uqVqAJwpGfffw8ZOPUegZvcsjEaVbsbMnEZVu4JchG+sjPPi7bTmXHhRAAHvzgo/vAeJtJpWmK4ExJpTPDvMgUbEqcAQ/+evAe2LzNqEilSQzPlGRaG+ZF5qHfDNxTrfJMNNRtJ7aC8Tajp+hs4Zmip7LDvMgUbFo2yiKEjx/u+cOuwcGXrXmD8QTyFMX+tOQVFYPxhC1v1+Dgy48f7vlDlYo04ejYEXv5j/syNj+sA3EFCgXuPS15xcBg3D77+cf/ze7s2BF7eZRbCIaPXxj4C/bIf4Z1ijSWgqnQevXpMBUNuZh9ShR75D87FWViNMoihADwk4Mf3KUahq3zjsWSFKZpDEzTRCxmWxeEahh/+MnBD+6qUpEmLPc9O3C7opm2WI39MQWmdbqFsGGaHAZi9l2iimZ23vds/+1VKtKEpfvuw1tMzbC9+WcGk+Cp/xsV3jSRGbT3f6ZmqN13Hy77lHzZhHB/UjY2HTz0A1jPFqoKBmI1755wVAZi8WFnBn908P3baznUUrHsP5Iz7n564HuwTJEqqo7+GJ1tHY3+WA6KfWNb193/NvC9/UdrPNRSkWhPHN8My6gQiobcAO2XGI3cQAqwj5o79I7jD1biu8smhADw8+7e3dt6P34KFjGUMznEEtQYWGKJFOuAoPPZ3t4nn+r+uGYj0JfKUy/Hdj37SqoDFjGUMxoGE7TxkWUwkWcdEHQ9+0qq46lXYjUb+LlUPv794NvGjvjvYBFDNZODFiOvWyxaLA2VWRc0dsR/1/vS4IHR7nGSsgohAHxv/3s/fSOe6IRFDJOpNBJJagwnSSTTSKZs9uh8PZ740237D9IuvRK5/Ym+R15/N/8nWMQwmdIQT9J69UniSRVJ+3nLrtffzf/p9if66MxqiRzd0vsc3snuhUUM86kMtARtHjyJlsggb3cj2cG9ndlbDg8yo1F2IQSAL72+59u9uZwtmnA8maLD9gCSKRlx5nhJby53+Muv7/k/VSrSpOMf7um+uadfZ9qfQoftMXRoPp60rwv29OuH/+Ge7purVKRJR/ddH27mjqt91rx8Ik2H7TF0aD6fsA+KuONq3+EfflRRxw0VEUIA+Pbb71yfNQzbTr5YIlnTI8NEMo2Y/dA8srqx89v737m+SkWatHznsb7rMnnTNs0XS+RremQYT6qIMWcsM3lz13ce67uuSkWatMibe3+EvPFTa54ST9f0yFBLZKDEmf4/b/xU3tzzo0qXpWJC2DmQjN+8b/86E9htzY8nUzW5ZhhLpCGUJnwAAAm3SURBVIaNBE1g981/2b+OAu46z2vvZeIbH+271jQ5W/iWeFKpyTXDwUR+2EjQNLk3Nz7Sdy0F3HWe/gNyPvfPPXdxJmzn4fKJdE2uGWqx9PCRoImtuX/uucvJgLtnSsWEEAB2HB/svnHf/jWsGCZTafQPxmriaIVpmugfjLFrgjCB3Tfu27dmx/HB7ioVbdKzc0+6+8bNvV9lxTCZ0nBiMF8TRytMk8OJAYVdE4Rpcm/esLl39c69aWp/ZeL4W6mB7P09dwwTw1QGSn9tHK3gTRNKf5JdEwRnYmv2/iN3HH8rNVCNchX9n1+KaK2YWtf68MIFj3t5frk13y1KiEZDcEti0c8ezwwdlk9CUexv4lnd2HnzX/avIxGsDCvaArMe+ta0n/vc3FJrviS60BCVIEkVfT+sGHnFwEBMYY9IIJM3d218pO9aEsHKMHVxsN7z3eZNcPM32D6QBPjqQuAkoUolKy+mog2dE2QcC/A545HMv/T831JEkONKe4mtihACQHt9KPLABec/1uTxzATQbv2sLhJGMOAr6fnjjVQ6M8xjDIDO3lzu8Lf3v3M9TYdWlsvm+SL/cv20J5obXDMBXGj9rD4iIRiYXC9jqbSKgfiwsFRdPf364e881ncdTYdWlob5frfvlqYfYoo4DcBa62eeaBCuoLdKJSsPeirLeowBgA7uuNonb+75UanToRNWCE/y60vaHrg4Em4HI4Y+rxfRUBCC6HLke6qFpuqIJVOs71CgcESCdodWl/+4o/XhS85zfxqMGPq8LtSF3RCEiT1dqmkmBhN51ncoUDgiQbtDq0vrplkbcb53ERgx5Hwe+CJ+QJjY/R80HZm4zPoOBQpHJJzaHTrhhRAA7l8w74bVTdPXgBFDAIiGQwgF/Y59VyVJpuRhu0ILdD7b2/sknRMcH9x73bSbrrkiuBaMGHIAImE3wsGJOVWVSCmIJ1SM8J/a9ewrqQ46Jzg+mLGh6Wp+ReTzYMTQ4Ex4wkGIoYk5O6YmR9gVOkSHsSP+OyfPCU4KIQSAb7Q2Lblr7pwfizz/GfYzSRAQCgXg902M6QI5k0UymWaD6gIY8h36o4Pv304eY8YXa5ZFl955bf39ksANexkTRQ6RoAS/b2IIopzREB8eVBfAkO/Qu/9t4HvkMWZ8Mf2zdRcI103byAkQwQgiL4qQQj64/O4qle7s0OU8lBGC6gLoMDVD1TuOP+i0x5hJI4QAsCDk578/d/ampXV1yzDC6NDtFhH0+8etIMqZLFKpDOsv9CSdfxyM7bz34KF/It+h45MFLR7+tmvq7/2bhb7lYEaHAOCWXAgFhHEriHJGQzKtIa+MGAi264//m91537P9t5Pv0PFL650zN6DNfykYMQQAQRIgBP3jVhB1OQ8tJUMbOcpGB/bIfy6XA+1JJYQnWTez+TO3zp51W0hwRTGCIIqCiIDfC7/fCxdf3R1+umEgLWchyzLUkSNRdyY1LfbAhx/9pOOjHnoLnwCsXRFdduuX6u4I+fgoRhBEUeAR8IsI+AS4qryEo+tAOqMgLetQtRH1rSshm7GHfjNwD4VSmhhMv6r+U65rGtbzPlcAIwgiBBekgBcuvwe8q7r9n6Eb0OUclHQWGLn/64BspNVtJ7aWM5TSpBTCk/z4/HPXf31G81qMIIYn8Xm98Hnd8Hk9JRvjTDFNE5lsDplsfqRNMFY6n+7p3XrH2wc7xrqIGJ/cvaZxw7XLQxswghiexOd1we8V4PW6wFeo/RmmiWxWh5wdFkCXpevpncktFFl+YtLyzekruSujXywkhwsihjbVuL1ucD6pYu0PBqBn88hn8yNtgjlJBwCYL8Z+U87I8ieZ1EIIAG0Rv3tda8tNqxobv4wxBBEAvB4PPG4JHkmC5HZ2+3sur0JRFOTyCrK504by6fztsWPbOg4f+emehFx7bksmEYvO8brXXRm5ddWSwGqMIYgA4PW44HEL8Lh5uB0+i5hXDOTyBnJ5DdncmOIHAF2/3Z1+puPF2Oa9H+Wo/U1wWm6ZsZpbGvq7QnJEQQQAweMG7xHBu0W4HO7/9LwKI6/CyKnQxm5SQwK4K/nSkYeObnO0EGMw6YXwJG1hv/trM5rXfqVp+rXckEecMUURGDqgL0oCREGAy+WC6HKB53nwPAeO504ZzzRNmIYJwzBhGAZUXYeu61A1DaqiQVUVnMGiSqcJGNt6e5965mjvkySAk4tF53jdX70ivH715aF/5DiTx2lEERhaUxRFDqLggiAAgouHi+eG2h/HgeOG/odMk4NpDrU/3TCh6QY0DVA1HapqQlH0kXZ+snSZJmdsezX55DOvxLeSAE4+Zmxoutq1PHKVycGFMQQRGNpxKokiOEkAN9T4wLt4uFw8wLvAcyaMQv/HmyYMkwMMHbpuwNANQNNhahpMRYOiquBP73WpgzOh6zvjL1QyasRJakYIrdw0u+Vzq6Y1fuE8v38hzkAQy0znu7K877d9x3716AdHXqpyWYgKcOPK+pWrlgS+Mr9FWogzEMQy03WgW923/X9Sv/zX5wfKPgVFVJ/mq6dcxC0Nfpaf6ZmD0whiBejAR7lD+n8nf9/zXP+bp7+8PNSkEJ6kvT4UWdHQsPKK+rrlc/z++aicKHYekuUDrwwM7txxov958gpTm1w2zxdZsdi/6vI235XnNknzUTlR7Hq/Vznw6p7MizvekreTV5japGG+3+25NLiUXxxYghnSzEJ2uYVxaL/DUeWw+WZ6d/b15K5qOMlmqWkhtLIg5Ofbo5GlF4fD7W3h4IUF121A6eLYCQzFCNyTSHW9kUh0dsbiu+gIBGFlQYuHv2y+9/KL53mWLJrjvajgug0oXRy7gKEYgXsPZd98473c7tcOZF/df4SOQBB2pl9V/ylxvncR5novKLhuA0oXxg5gKEag+X72bfVAdm85d38WCwnhGKyYWtc6x+ed1+r1zmryemdMkaQpdaJQFxDEkJfnfALPiwCgGYaaNYxMWtOSg6o2eCKvnOjNZY92Z7MfHspk3yNn2EQxrGgLzJo9zT23Zaowq6lebJ0a4adFg0Jd0MeFPBLnEwRuqP1ppppTzEwqYyZjKW3weNzo6x1Qu7uPax9+2Jc/uGNP+sNq/xZi4jF1cbBebJZa+UapiZ8iNiIi1JshV8Tw8wFOcrl511D0IUOHweWNPDJ6mkvqccS1AeOEesw4pvSqPUp3tSJCnA2VOjFAEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEAQxzvj/snGtbrdYI/0AAAAASUVORK5CYII=);
height: 32px;
width: 100%;
background-size: 40px;
background-repeat: no-repeat;
margin-bottom: 0;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
}
.code-block {
position: relative;
overflow-x: visible !important;
&:before {
content: attr(lang);
font-size: 12px;
position: absolute;
top: -34px;
right: 0;
line-height: 1;
z-index: 1;
}
}
.code-inline {
margin: 0 2px;
}
pre, pre div {
background: #1a1a1a !important;
}
img {
background: none !important;
}
* {
// text wrap
word-wrap: anywhere;
word-break: break-word;
}
}
.bing {
display: inline-flex;
flex-direction: row;
align-items: center;
vertical-align: center;
gap: 6px;
color: #2f7eee;
background: #e8f2ff;
border-radius: 12px;
padding: 6px 12px;
font-size: 16px;
margin: 6px 0;
width: max-content;
user-select: none;
word-wrap: anywhere;
max-width: 100%;
svg {
width: 20px;
height: 20px;
flex-shrink: 0;
}
}
================================================
FILE: app/src/assets/pages/generation.less
================================================
.generation-page {
position: relative;
display: flex;
width: 100%;
height: calc(100% - var(--navbar-height));
.login-action {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin: auto;
transform: translateY(-28px);
.tip {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: max(6vh, 24px);
user-select: none;
}
.text {
font-size: 1rem;
}
}
}
.generation-container {
display: flex;
flex-direction: column;
padding: 12px 16px;
gap: 6px;
width: 100%;
height: 100%;
.action {
flex-shrink: 0;
}
.generation-wrapper {
display: flex;
flex-direction: column;
align-items: center;
flex-grow: 1;
padding: 15vh 0;
.box {
display: flex;
flex-direction: column;
align-items: center;
width: 80%;
max-width: 680px;
height: max-content;
margin: 6px 0;
gap: 12px;
.message-box {
width: 100%;
height: max-content;
min-height: 120px;
border-radius: var(--radius);
border: 1px solid hsl(var(--border));
color: hsl(var(--text-secondary));
padding: 0.6rem 1rem;
font-size: 10px;
overflow: hidden;
text-overflow: ellipsis;
}
.quota-box {
display: flex;
flex-direction: row;
align-items: center;
width: max-content;
height: max-content;
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
user-select: none;
padding: 4px 12px;
transition: .2s;
cursor: pointer;
&:hover {
border: 1px solid hsl(var(--border-hover));
}
}
.hash-box {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
width: max-content;
height: max-content;
padding: 1rem 1.5rem;
.download-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
min-width: 10rem;
width: max-content;
height: max-content;
padding: 1rem 1.6rem;
text-decoration: none;
cursor: pointer;
user-select: none;
border-radius: var(--radius);
border: 1px solid hsl(var(--border));
transition: .2s;
color: hsl(var(--text));
font-size: 1rem;
font-weight: 500;
background: hsla(var(--background-container));
&:hover {
border: 1px solid hsl(var(--border-hover));
}
}
}
}
.product {
display: flex;
flex-direction: row;
align-items: center;
text-align: center;
font-size: min(2rem, 7vw);
gap: 12px;
user-select: none;
white-space: nowrap;
img {
width: min(3rem, 14vw);
height: min(3rem, 14vw);
}
}
}
}
.model-box {
width: 80%;
margin: 0 auto;
max-width: 680px;
}
.generate-box {
display: flex;
flex-direction: row;
width: 80%;
margin: 2rem auto;
max-width: 680px;
.input {
flex-grow: 1;
text-align: center;
font-size: 1.25rem;
height: 46px;
border-radius: var(--radius);
border: 1px solid hsl(var(--border-hover));
letter-spacing: 1px;
margin-right: 8px;
}
.action {
width: 46px;
height: 46px;
}
}
================================================
FILE: app/src/assets/pages/home.less
================================================
.main {
position: relative;
display: inline-flex;
flex-direction: row;
width: 100%;
height: calc(100% - var(--navbar-height));
overflow: hidden;
@media (orientation: portrait) {
flex-direction: column-reverse;
.home-page {
height: calc(100% - 9rem);
width: 100%;
}
.toolbar {
flex-direction: row;
height: max-content;
width: 100%;
border-right: none;
border-top: 1px solid hsl(var(--border));
& > * {
margin-bottom: 0;
margin-right: auto;
&:last-child {
margin-right: 0.5rem;
}
&:first-child {
margin-left: 0.5rem;
}
}
}
.sidebar.open {
width: 100% !important;
}
.sidebar .sidebar-menu {
display: none;
}
}
}
.model-market {
display: flex;
flex-direction: column;
width: 100%;
padding: 0 1.5rem;
height: 100%;
overflow: auto;
scrollbar-width: thin;
.market-wrapper {
width: 100%;
height: 100%;
margin: 0 auto;
max-width: 1400px;
}
@media (max-width: 768px) {
padding: 0 1rem;
}
.title {
font-size: 24px;
}
& > * {
flex-shrink: 0;
}
.search-bar-wrapper {
margin: 1rem auto;
width: calc(100% - 1rem);
.search-bar {
position: relative;
margin-bottom: 0.5rem;
}
.search-icon {
position: absolute;
top: 50%;
left: 1rem;
transform: translateY(-50%);
width: 1rem;
height: 1rem;
}
.clear-icon {
position: absolute;
top: 50%;
right: 1rem;
transform: translateY(-50%);
width: 1rem;
height: 1rem;
opacity: 0;
transition: 0.25s;
cursor: pointer;
&.active {
opacity: .8;
&:hover {
opacity: 1;
}
}
}
.input-box {
padding: 0 2.5rem;
}
}
.model-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
justify-content: center;
width: 100%;
@media (max-width: 1024px) {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}
.model-item {
position: relative;
display: flex;
flex-direction: column;
user-select: none;
padding: 0.5rem 0.5rem;
margin: 0.5rem;
border: 1px solid hsl(var(--border-hover));
transition: 0.25s;
transition-property: border-color, padding, background, box-shadow;
cursor: pointer;
animation: fadein 0.25s forwards ease-in-out;
opacity: 0;
width: calc(100% - 1rem);
min-width: 0;
svg {
flex-shrink: 0;
}
@keyframes fadein {
from { opacity: 0; transform: translateY(2.5rem); }
to { opacity: 1; transform: translateY(0); }
}
.model-name {
color: hsl(var(--text));
font-weight: medium;
}
.model-id {
color: hsl(var(--text-secondary));
font-size: 14px;
margin-top: 0.25rem;
background: hsl(var(--muted));
}
.model-description {
color: hsl(var(--text-secondary));
margin-bottom: 0.25rem;
}
&:hover {
background: hsla(var(--background-hover));
border-color: hsl(var(--border-active));
.grip-icon {
opacity: 1;
}
}
&.active {
border-color: hsl(var(--border-active));
box-shadow: 0 0 0 1px hsl(var(--text-secondary));
.grip-icon {
opacity: 1;
}
}
.grip-icon {
opacity: 0.6;
transition: 0.25s;
}
.model-avatar {
border-radius: 50%;
width: 3rem;
height: 3rem;
@media (max-width: 768px) {
width: 2.5rem;
height: 2.5rem;
}
}
.model-action {
button {
background: none !important;
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
25% {
transform: rotate(25deg);
}
50% {
transform: rotate(0deg);
}
75% {
transform: rotate(-25deg);
}
100% {
transform: rotate(0deg);
}
}
svg {
animation: rotate 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955);
}
}
.market-tip {
transform: translateY(1px);
}
.model-name {
display: flex;
flex-direction: row;
align-items: center;
.badge {
transform: translateY(-2px);
}
&.pro {
p {
// gold color gradient
background: linear-gradient(to right, hsl(45, 100%, 70%) 0%, hsl(46, 100%, 58%) 50%, hsl(46, 100%, 50%) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.badge {
color: rgb(164, 128, 0) !important;
background: rgb(255, 231, 145) !important;
}
}
}
.model-tag {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-top: 0.25rem;
transform: translateY(0.25rem);
}
}
.market-header {
position: relative;
width: 100%;
height: max-content;
margin-top: 1.5rem;
.header-bar {
width: 0.75rem;
margin: 0 1rem;
aspect-ratio: .75;
background:
var(--anim-bar) 0% 50%,
var(--anim-bar) 50% 50%,
var(--anim-bar) 100% 50%;
animation: l7 1s infinite linear alternate;
@keyframes l7 {
0% {background-size: 20% 50% ,20% 50% ,20% 50% }
20% {background-size: 20% 20% ,20% 50% ,20% 50% }
40% {background-size: 20% 100%,20% 20% ,20% 50% }
60% {background-size: 20% 50% ,20% 100%,20% 20% }
80% {background-size: 20% 50% ,20% 50% ,20% 100%}
100%{background-size: 20% 50% ,20% 50% ,20% 50% }
}
&.reverse {
background:
var(--anim-bar) 100% 50%,
var(--anim-bar) 50% 50%,
var(--anim-bar) 0% 50%;
}
}
.close-action {
position: absolute;
}
}
.market-footer {
margin-top: 3rem;
margin-bottom: 2.5rem;
user-select: none;
padding: 0 1rem;
a {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
font-size: 14px;
color: hsl(var(--text-secondary));
transition: 0.25s;
width: max-content;
max-width: 100%;
margin: 0 auto;
text-align: center;
svg {
flex-shrink: 0;
}
&:hover {
color: hsl(var(--text));
}
}
}
}
.conversation-name {
color: hsl(var(--text));
font-weight: bold !important;
word-wrap: anywhere;
}
.chat-action {
@keyframes up {
0% {
opacity: 0;
transform: translateY(100%);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
animation: up 0.2s ease-in-out;
}
.web {
color: hsl(var(--input-unread));
transition: .25s linear;
&.enable {
color: hsl(var(--text));
}
}
.toolbar {
position: relative;
display: flex;
flex-shrink: 0;
flex-direction: column;
height: 100%;
margin: 0;
width: max-content;
padding: 0.5rem;
background: hsl(var(--background));
border-right: 1px solid hsl(var(--border));
transition: 0.25s;
&.stacked {
@media (orientation: landscape) {
width: 0;
padding-left: 0;
padding-right: 0;
border-right: 0;
}
@media (orientation: portrait) {
height: 0;
padding-top: 0;
padding-bottom: 0;
border-top: 0;
}
.toolbar-text {
display: none;
}
button {
width: 0;
flex-shrink: 1;
}
}
.bar-kit {
position: absolute;
opacity: 0;
transition: 0.25s ease-in-out;
cursor: pointer;
z-index: 64;
border-radius: 0.25rem;
background: hsl(var(--background));
width: max-content;
height: max-content;
border: 1px solid hsl(var(--border-active));
@media (orientation: portrait) {
top: 0;
left: 50%;
transform: translate(-50%, -100%);
padding: 0 0.75rem;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
svg {
rotate: 0deg;
}
&.stacked svg {
rotate: 180deg;
}
}
@media (orientation: landscape) {
top: 50%;
right: 0;
transform: translate(100%, -50%);
padding: 0.75rem 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
svg {
rotate: 90deg;
}
&.stacked svg {
rotate: -90deg;
}
}
}
&:hover, &:focus-within, &:focus, &:active {
.bar-kit {
opacity: 1;
pointer-events: all;
}
}
& > * {
margin-bottom: 0.75rem;
&:last-child {
margin-bottom: 0;
}
}
}
.sidebar {
display: flex;
flex-shrink: 0;
flex-direction: column;
width: 0;
height: 100%;
padding: 0;
margin: 0;
background: hsl(var(--background));
transition: 0.2s ease-in-out;
transition-property: width, background, box-shadow, border-right, opacity;
border-right: 0;
pointer-events: none;
opacity: 0;
overflow-x: hidden;
&.open {
width: 260px;
border-right: 1px solid hsl(var(--border));
pointer-events: auto;
opacity: 1;
}
&.hidden {
width: 0;
border-right: 0;
}
.sidebar-content {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding: 4px;
}
.sidebar-menu {
height: max-content;
width: 100%;
.sidebar-wrapper {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
height: max-content;
width: calc(100% - 0.5rem);
margin: 0.25rem;
img {
width: 2.5rem;
height: 2.5rem;
padding: 0.2rem;
border-radius: .5rem;
transform: translateY(0.05rem);
flex-shrink: 0;
}
.username {
margin: 0 auto 0 8px;
color: hsl(var(--text));
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
font-family: var(--font-family-normal);
}
svg {
color: hsl(var(--text-secondary));
}
}
}
.conversation-list {
position: relative;
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
height: 100%;
padding: 6px 0;
overflow-x: hidden;
overflow-y: auto;
touch-action: pan-y;
user-select: none;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
transition: 0.2s ease-in-out;
.empty {
color: hsl(var(--text-secondary));
font-size: 14px;
margin: auto;
user-select: none;
}
.conversation {
display: flex;
flex-direction: row;
vertical-align: center;
align-items: center;
width: calc(100% - 12px);
height: max-content;
cursor: pointer;
margin: 0 6px;
padding: 10px 12px;
border-radius: var(--radius);
border: 1px solid hsl(var(--border));
transition: 0.2s ease-in-out;
background: hsl(var(--card));
.more {
color: hsl(var(--text-secondary));
scale: 0;
opacity: 1;
transition: 0.2s;
transition-property: color, opacity;
border: 1px solid var(--border);
outline: none;
height: 0;
width: 0;
&:hover {
color: hsl(var(--text));
}
}
&:hover {
border-color: hsl(var(--border-active));
.id {
color: hsl(var(--text));
}
}
&.active {
background: hsl(var(--card-hover));
border-color: hsl(var(--border-active));
.id {
color: hsl(var(--text));
}
}
}
.id {
color: hsl(var(--text-unread));
}
svg {
flex-shrink: 0;
}
.title {
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 16px;
user-select: none;
margin: 0 4px;
color: hsl(var(--text));
}
&::-webkit-scrollbar {
width: 6px;
}
}
.sidebar-action {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
margin-bottom: 0.25rem;
.refresh-action {
&.active {
svg {
animation: RotateAnimation 0.5s linear infinite;
@keyframes RotateAnimation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
}
}
}
}
.login-action {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
width: calc(100% - 1rem);
margin: auto auto 0.5rem;
svg {
transform: translateY(1px);
}
}
@media (max-width: 768px) {
&.open {
width: max(30vw, 180px);
}
}
@media (max-width: 468px) {
// sidebar collapsed
&.open {
width: calc(100% - 3.5rem) !important;
}
&.open ~ .chat-container {
width: 0;
}
}
}
.chat-container {
flex: 1 1 auto;
min-width: 0;
height: 100%;
transition: width 0.2s ease-in-out;
.chat-wrapper {
display: inline-flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.tooltip {
user-select: none;
strong {
font-weight: 600 !important;
}
}
.chat-product {
position: relative;
display: flex;
flex-direction: column;
flex-grow: 1;
width: 100%;
overflow: hidden;
justify-content: center;
align-items: center;
button {
margin: 0.5rem 0;
white-space: nowrap;
}
.space-footer {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
bottom: 6px;
user-select: none;
color: hsl(var(--text-secondary));
padding: 1rem;
z-index: 10;
* {
font-size: 14px;
}
p {
white-space: pre-wrap;
text-align: center;
&:first-child {
margin-bottom: 4px;
}
}
a {
color: hsl(var(--text));
text-decoration: none;
transition: 0.25s;
cursor: pointer;
}
}
}
.chat-content {
overscroll-behavior: none;
flex: 1 1;
width: 100%;
overflow: hidden;
.chat-messages-wrapper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 18px 24px;
}
.message {
margin-bottom: 0.75rem;
&:last-child {
margin-bottom: 0;
}
}
}
.chat-input {
flex-shrink: 0;
width: 100%;
overflow: hidden;
.input-wrapper {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
width: 100%;
height: min-content;
.chat-box {
position: relative;
flex-grow: 1;
}
.input-box {
resize: none;
width: 100%;
color: hsl(var(--text));
white-space: pre-wrap;
padding-right: 3.25rem;
&.align {
text-align: center;
}
&::placeholder {
color: hsl(var(--text-secondary));
opacity: 1;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
&:active::placeholder,
&:focus::placeholder {
opacity: 0;
visibility: hidden;
}
@-moz-document url-prefix() {
&::-moz-placeholder {
opacity: 1;
transition: opacity 0.3s ease, visibility 0.3s ease;
visibility: visible;
}
&:active::-moz-placeholder,
&:focus::-moz-placeholder {
opacity: 0;
visibility: hidden;
}
}
}
.action-button {
position: absolute;
right: 0.75rem;
bottom: 0.75rem;
}
}
.input-options {
margin: 12px auto -8px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 4px;
height: min-content;
}
}
}
.share-wrapper {
display: flex;
flex-direction: row;
gap: 6px;
width: 100%;
input {
text-align: center;
font-size: 16px;
cursor: pointer;
flex-grow: 1;
}
button {
flex-shrink: 0;
}
}
.contact-image {
margin-top: 0.75rem;
max-width: min(60vw, 420px);
max-height: 50vh;
border-radius: var(--radius);
}
.version {
display: flex;
flex-direction: row;
align-items: center;
user-select: none;
font-size: 14px;
color: hsl(var(--text-secondary));
transform: translateY(4px);
width: max-content;
margin: 0 auto;
.app {
margin-right: 2px;
padding: 2px;
width: 24px;
height: 24px;
color: hsl(var(--text-secondary));
cursor: pointer;
transition: 0.25s;
rotate: 0;
&:hover {
color: hsl(var(--text));
rotate: 30deg;
}
}
p {
font: var(--font-family-normal);
transform: translateY(-1px);
}
}
.tag-item {
flex-shrink: 0;
margin-right: 0.25rem;
padding: 0.15rem 0.5rem;
border: 1px solid hsl(var(--text-secondary) / 0.25);
background: hsl(var(--muted));
border-radius: 4px;
font-size: 12px;
margin-bottom: 0.25rem;
transition: 0.2s;
&.clickable {
cursor: pointer;
}
&:hover {
background: hsl(var(--muted-foreground) / 0.25);
color: hsl(var(--text));
}
&.pro {
color: hsl(var(--gold)) !important;
}
&:last-child {
margin-right: 0;
}
}
.conversation-id {
color: hsl(var(--text));
&:before {
content: "#";
color: hsl(var(--text-secondary));
font-size: 12px;
}
}
================================================
FILE: app/src/assets/pages/navbar.less
================================================
.navbar {
display: flex;
flex-direction: row;
align-content: center;
vertical-align: center;
user-select: none;
height: var(--navbar-height);
padding: 0.5rem 0.5rem;
background: hsl(var(--background));
border-bottom: 1px solid hsl(var(--border));
.items {
width: 100%;
height: 100%;
margin: 0;
padding-right: 0.5rem;
display: flex;
flex-direction: row;
align-content: center;
vertical-align: center;
}
.logo {
border-radius: var(--radius);
cursor: pointer;
}
button {
white-space: nowrap;
}
}
.avatar {
outline: 0;
user-select: none;
img {
cursor: pointer;
}
}
div[data-radix-popper-content-wrapper=""] {
user-select: none;
div.relative {
cursor: pointer;
}
.username {
color: hsl(var(--text));
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 120px;
&:before {
content: "@";
font-size: 12px;
margin-right: 1px;
color: hsl(var(--text-secondary));
}
}
.action-button {
width: calc(100% - 4px);
cursor: pointer;
margin: 8px 2px 2px;
height: max-content !important;
}
}
================================================
FILE: app/src/assets/pages/notify.less
================================================
.notify-container {
width: 100%;
height: 100%;
overflow: hidden;
background: hsla(var(--background-container));
}
================================================
FILE: app/src/assets/pages/package.less
================================================
.package-wrapper {
display: flex;
flex-direction: column;
.package {
display: flex;
flex-direction: column;
gap: 8px;
.package-title {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
font-size: 0.85rem;
color: hsl(var(--text));
user-select: none;
svg {
transform: translateY(1px);
}
}
.package-content {
font-size: 0.8rem;
color: hsl(var(--text-secondary));
}
}
}
================================================
FILE: app/src/assets/pages/preset.less
================================================
.mask-container {
height: max-content;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: thin;
padding: 1rem 0.5rem 0.5rem;
}
.mask-drawer-viewport {
width: 80vw;
@media (max-width: 768px) {
margin-left: 0.5rem;
margin-right: 0.5rem;
width: calc(100% - 1rem) !important;
}
}
.mask-picker-dialog {
width: max-content !important;
height: max-content !important;
padding: 0 !important;
.picker {
--epr-category-navigation-button-size: 28px;
--epr-search-input-bg-color: hsl(var(--background));
--epr-search-border-color: hsl(var(--border-hover));
--epr-bg-color: hsl(var(--background));
--epr-category-label-bg-color: hsl(var(--background));
img {
padding: 0.5rem;
}
.epr-icn-search {
width: 1rem;
height: 1rem;
transform: translateY(-0.55rem);
}
.epr-body {
scrollbar-width: thin;
-ms-overflow-style: none;
&::-webkit-scrollbar {
width: 6px;
}
}
.epr-search-container {
input {
border: 1px solid hsl(var(--border));
}
}
.epr-cat-btn {
&:focus:before {
border: none;
}
}
* {
font-family: var(--font-family) !important;
font-size: 0.85rem !important;
}
}
}
.mask-editor-container {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding: 2rem 0;
.mask-conversation-list {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
height: max-content;
padding: 0.5rem 1.5rem;
margin: 1rem 0;
border-radius: var(--radius);
border: 1px solid hsl(var(--border));
color: hsl(var(--text));
.mask-conversation-title {
margin-top: 0.5rem;
margin-bottom: 1rem;
}
.mask-conversation-wrapper {
display: flex;
flex-direction: column;
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
}
.mask-conversation {
display: flex;
flex-direction: row;
padding: 0.25rem;
}
.mask-conversation:hover ~ .mask-actions {
.mask-action {
border-color: hsl(var(--border-hover));
}
}
.mask-actions {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-top: 0.5rem;
.mask-action {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border: 1px solid hsl(var(--border));
width: 36px;
height: 24px;
border-radius: 12px;
margin: 0 0.25rem;
transition: 0.2s;
svg {
height: 16px;
width: 16px;
padding: 1px;
}
&.disabled {
cursor: not-allowed;
}
&:hover:not(.disabled) {
border-color: hsl(var(--border-hover));
background-color: hsl(var(--background-hover));
}
}
}
}
.mask-editor-row {
display: flex;
flex-direction: column;
width: 100%;
border-radius: var(--radius);
border: 1px solid hsl(var(--border));
}
.mask-editor-column {
display: flex;
flex-direction: row;
width: 100%;
height: max-content;
padding: 0.5rem;
color: hsl(var(--text));
align-items: center;
border-bottom: 1px solid hsl(var(--border));
&:last-child {
border-bottom: none;
}
& > p {
margin-left: 1rem;
user-select: none;
white-space: nowrap;
}
& > * {
margin-right: 0.5rem;
&:last-child {
margin-right: 0;
}
}
}
}
.mask-wrapper {
display: flex;
flex-direction: column;
.mask-header {
display: flex;
flex-direction: row;
width: 100%;
}
.mask-col {
display: flex;
flex-direction: column;
margin: 0.5rem 0;
width: 100%;
.mask-col-title {
color: hsl(var(--text));
margin: 0.5rem auto 0.5rem 0.5rem;
}
}
}
.mask-list {
display: flex;
flex-direction: column;
user-select: none;
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
overflow: hidden;
&::-webkit-scrollbar {
width: 0.25rem;
}
.mask-item {
&:last-child {
border-bottom: none;
}
}
}
.mask-item {
display: flex;
flex-direction: row;
align-items: center;
flex-shrink: 0;
cursor: pointer;
border-bottom: 1px solid hsl(var(--border));
padding: 1rem 0;
transition: 0.2s ease-in-out;
.mask-avatar {
width: 2.25rem;
height: 2.25rem;
padding: 0.5rem;
margin-right: 0.75rem;
margin-left: 1rem;
border-radius: var(--radius);
border: 1px solid hsl(var(--border));
transition: 0.2s ease-in-out;
}
.mask-content {
display: flex;
flex-direction: column;
.mask-name {
color: hsl(var(--text));
margin-right: 0.5rem;
}
.mask-info {
font-size: 12px;
color: hsl(var(--text-secondary));
white-space: nowrap;
max-width: max-content;
}
}
&:hover {
background-color: hsl(var(--background-hover));
.mask-avatar {
border-color: hsl(var(--border-active));
}
}
}
================================================
FILE: app/src/assets/pages/quota.less
================================================
.buy-interface {
display: flex;
flex-direction: row;
flex-wrap: wrap;
flex-basis: auto;
flex-shrink: 0;
width: 100%;
height: max-content;
}
.buy-action {
width: 100%;
margin-top: 1.5rem;
.buy-button {
width: 100%;
transition: .25s;
}
}
.quota-dialog {
max-width: min(90vw, 680px) !important;
}
.amount-container {
display: flex;
flex-direction: column;
gap: 24px;
width: 100%;
align-items: center;
}
.other-wrapper {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
gap: 12px;
user-select: none;
@media (max-width: 460px) {
& {
flex-direction: column;
}
}
.amount-number {
color: hsl(var(--text));
transform: translateY(-2px);
white-space: nowrap;
}
.amount-input-box {
position: relative;
width: max-content;
max-width: 100%;
height: max-content;
margin: 0 auto;
.amount-input {
color: hsl(var(--text));
font-size: 16px;
text-align: center;
}
svg {
color: hsl(var(--text));
position: absolute;
top: 50%;
left: 12px;
user-select: none;
transform: translateY(-50%);
}
}
}
.line {
background: hsl(var(--border));
width: 1px;
min-height: 0;
height: 100%;
@media (max-width: 980px) {
& {
display: none;
}
}
}
.amount-wrapper {
display: inline-grid;
gap: 12px;
justify-content: center;
width: 100%;
grid-template-columns: 1fr 1fr 1fr 1fr;
@media (max-width: 760px) {
grid-template-columns: 1fr 1fr;
}
@media (max-width: 380px) {
grid-template-columns: 1fr;
}
.amount {
position: relative;
display: flex;
padding: 1rem 0.5rem;
flex-direction: column;
align-items: center;
justify-content: center;
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
user-select: none;
cursor: pointer;
gap: 4px;
min-width: 6rem;
width: 100%;
transition: .1s linear;
text-align: center;
&.active {
border-color: hsl(var(--text-secondary));
.amount-desc,
.other {
color: hsl(var(--text));
}
}
.amount-title {
display: flex;
flex-direction: row;
gap: 2px;
color: hsl(var(--text));
font-size: 16px;
align-items: center;
svg {
transform: translateY(1px);
width: 14px;
height: 14px;
}
}
.amount-desc {
color: hsl(var(--text-secondary));
transition: .1s linear;
}
.other {
font-size: 12px;
color: hsl(var(--text-secondary));
transition: .1s linear;
&:after {
content: '...';
font-size: 10px;
}
}
}
}
.grow {
flex-grow: 1;
}
.product-item {
display: flex;
flex-direction: column;
padding: 4px 2px;
width: 100%;
gap: 4px;
.row {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
.column {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
svg {
flex-shrink: 0;
}
}
.info {
margin-top: 6px;
}
}
.title {
color: hsl(var(--text));
}
.desc {
color: hsl(var(--text-secondary));
}
}
.quota-tip {
color: hsl(var(--text));
text-align: center;
align-items: center;
a {
display: flex;
flex-direction: row;
align-items: center;
}
}
================================================
FILE: app/src/assets/pages/record.less
================================================
.record-wrapper > * {
max-width: 100%;
}
.record-area {
display: block !important;
}
.stats-boxes {
.stats-box {
display: flex;
flex-direction: row;
flex-grow: 1;
height: max-content;
padding: 0.75rem 1.5rem;
border-radius: var(--radius);
background: hsl(var(--muted) / 0.25);
box-shadow: 0.5rem 0.5rem 1rem 0 var(--shadow);
user-select: none;
width: 100%;
margin-right: 1.5rem;
margin-left: auto;
&:last-child {
margin-right: auto;
}
& > * {
flex-shrink: 0;
}
.box-wrapper {
flex-grow: 1;
.box-title {
font-size: 1rem;
margin-bottom: 0.5rem;
}
.box-value {
font-size: 1.5rem;
font-weight: normal;
}
}
.box-icon {
width: max-content;
height: max-content;
transform: translate(0.25rem, 0.25rem);
border-radius: 0.25rem;
svg {
width: 2rem;
height: 2rem;
stroke-width: 1;
}
}
}
}
================================================
FILE: app/src/assets/pages/settings.less
================================================
.settings-dialog {
max-width: min(90vw, 660px) !important;
}
.settings-container {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
height: 100%;
.info-box {
display: flex;
flex-direction: column;
align-items: center;
user-select: none;
color: hsl(var(--text));
& > * {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
}
}
}
.settings-wrapper {
width: 100%;
height: max-content;
margin: 1.5rem 0.5rem;
.settings-segment {
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
}
}
.settings-segment {
display: flex;
flex-direction: column;
width: 100%;
color: hsl(var(--text));
border: 1px solid hsl(var(--border));
border-radius: var(--radius);
padding: 0.75rem 0;
.item {
display: flex;
flex-direction: row;
align-items: center;
user-select: none;
padding: 0 1rem;
.value {
transition: .1s;
}
.slider-value {
min-width: 2rem;
}
input {
text-align: center;
max-width: 4rem;
max-height: 1.75rem;
&.large-value {
max-width: 6rem;
}
}
button:not(.tips-trigger):not(.set-action) {
margin: 0.25rem 1rem;
}
.select {
margin: 0 !important;
padding: 0.25rem 0.75rem !important;
span {
font-size: 0.8rem !important;
margin: 0 0.5rem;
}
}
.name {
display: flex;
flex-direction: row;
text-align: start;
align-items: center;
.tips-trigger {
transform: translateY(1px);
}
}
}
& > .item {
margin-bottom: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid hsl(var(--border));
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: 0;
}
}
}
================================================
FILE: app/src/assets/pages/share-manager.less
================================================
.share-table {
width: 100%;
height: max-content;
}
================================================
FILE: app/src/assets/pages/sharing.less
================================================
================================================
FILE: app/src/assets/pages/subscription.less
================================================
.sub-dialog {
width: max-content !important;
max-width: min(90vw, 1200px) !important;
}
.sub-wrapper {
display: flex;
flex-direction: column;
align-items: center;
}
.sub-row {
display: flex;
flex-direction: column;
padding: 0.5rem 1rem;
margin-bottom: 8px;
align-items: center;
width: 100%;
.sub-column {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 4px;
width: 100%;
svg {
flex-shrink: 0;
transform: translateY(1px);
}
.sub-value {
display: flex;
flex-direction: row;
align-items: center;
gap: 2px;
p {
font-weight: bolder;
}
}
&:first-child {
margin-bottom: 8px;
}
&:last-child {
margin-bottom: 0;
}
}
}
.plan-wrapper {
margin-top: 0.5rem;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 1rem;
justify-content: center;
width: 100%;
@media (max-width: 870px) {
grid-template-columns: 1fr;
}
&.disable {
grid-template-columns: 1fr;
}
}
.plan {
display: flex;
flex-direction: column;
gap: 6px;
padding: 1rem;
color: hsl(var(--text));
width: 100%;
&.standard {
border-color: hsl(var(--text-secondary));
}
.title {
text-align: center;
font-size: 16px;
margin: 4px;
}
.award {
display: flex;
flex-direction: column;
color: hsl(var(--gold));
justify-content: center;
align-items: center;
user-select: none;
font-size: 0.75rem;
line-height: 0.8rem;
margin-bottom: 1rem;
padding: 0.5rem;
border-radius: var(--radius);
border: 1px solid hsl(var(--gold));
svg {
flex-shrink: 0;
transform: translateY(1px);
}
div {
word-break: break-word;
white-space: break-spaces;
text-align: center;
}
}
.price-wrapper {
position: relative;
width: max-content;
margin: 0 auto;
.price {
font-size: 18px;
font-weight: bold;
margin: 2px auto;
.tax {
color: hsl(var(--text-secondary));
}
}
.annotate {
position: absolute;
font-size: 14px;
margin-top: 0.25rem;
font-weight: normal;
color: hsl(var(--text-secondary));
right: 0;
bottom: 0;
transform: translateX(calc(100% + 0.1rem));
}
}
.desc {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin: 0.5rem 0;
.api-tip {
display: flex;
flex-direction: column;
align-items: center;
}
div {
svg {
flex-shrink: 0;
}
}
}
.action {
margin-top: auto !important;
}
}
.upgrade-wrapper {
margin: 24px auto 8px;
.price {
font-size: 14px;
margin-top: 12px;
text-align: center;
transform: translateY(12px);
.tax {
color: hsl(var(--text-secondary));
}
}
}
@media (max-width: 460px) {
.plan {
min-width: 0 !important;
}
}
================================================
FILE: app/src/assets/ui.less
================================================
@import "fonts/all";
.gold-text {
color: hsl(var(--gold)) !important;
}
.spinner {
position: absolute;
top: 0;
left: 0;
height: 2px;
background: hsl(var(--text));
transition: 0.25s linear;
transition-property: width, opacity;
z-index: 1024;
user-select: none;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
}
.select-group {
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding: 6px 8px;
border-radius: 4px;
user-select: none;
justify-content: center;
&.mobile {
text-align: center;
& span {
margin: 0 auto;
}
}
.select-group-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 0.35rem 0.8rem;
margin: 0.25rem;
border: 1px solid hsl(var(--border));
border-radius: 4px;
transition: .2s;
cursor: pointer;
font-size: 16px;
background: hsl(var(--background));
color: hsl(var(--text));
&:hover {
background: hsl(var(--accent-secondary));
}
&.active {
border-color: hsl(var(--border-hover));
background: hsl(var(--accent));
}
}
}
.select-element.badge {
user-select: none;
transition: .2s;
padding-left: 0.45rem;
padding-right: 0.45rem;
&.badge-default {
background: hsl(var(--primary)) !important;
&:hover {
background: hsl(var(--primary)) !important;
}
}
&.badge-gold {
color: rgb(164, 128, 0) !important;
background: rgb(255, 231, 145) !important;
}
}
.no-scrollbar {
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: transparent;
}
}
.thin-scrollbar {
scrollbar-width: thin;
-ms-overflow-style: none;
&::-webkit-scrollbar {
width: 6px;
}
// update the scrollbar style
&::-webkit-scrollbar-thumb {
background: hsl(var(--border));
border-radius: 6px;
}
&::-webkit-scrollbar-thumb:hover {
background: hsl(var(--border-hover));
}
&::-webkit-scrollbar-track {
background: hsl(var(--background));
}
}
.horizontal-scrollbar {
--radix-scroll-area-thumb-height: 6px;
}
input[type="number"] {
-webkit-appearance: textfield;
margin: 0;
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
-webkit-appearance: none;
}
&::after {
content: '>';
position: absolute;
right: 5px;
top: 2px;
transform: rotate(-45deg);
}
&::before {
content: '<';
position: absolute;
right: 5px;
top: 20px;
transform: rotate(135deg);
}
}
.selection {
::selection {
color: hsl(var(--selection-foreground));
background: hsl(var(--selection));
}
::-moz-selection {
color: hsl(var(--selection-foreground));
background: hsl(var(--selection));
}
}
.paragraph {
display: flex;
flex-direction: column;
margin: 0.5rem 0;
padding: 0.5rem;
border-bottom: 1px solid hsl(var(--border));
color: hsl(var(--text));
transition: .25s;
cursor: pointer;
&.collapsable {
.paragraph-content {
max-height: var(--max-height);
will-change: max-height;
overflow: hidden;
transition: .5s;
}
&.collapsed {
padding-bottom: 1rem;
.paragraph-content {
max-height: 0;
}
}
}
.paragraph-content {
& > * {
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
}
}
.paragraph-header {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
transform: translateY(-0.25rem);
}
.paragraph-title {
position: relative;
display: flex;
flex-direction: row;
font-size: 1.05rem;
user-select: none;
line-height: 1.1rem;
color: hsl(var(--text-secondary));
transition: .25s;
text-decoration: none !important;
&:before {
content: '';
margin-right: 0.75rem;
height: 1.25rem;
width: 2px;
border-radius: 1px;
background: hsl(var(--text-secondary));
transition: .25s;
}
}
.paragraph-item {
display: flex;
flex-direction: row;
white-space: nowrap;
align-items: center;
font-size: 0.9rem;
&.row-layout {
align-items: flex-start !important;
flex-direction: column !important;
& > * {
margin-right: 0 !important;
margin-bottom: 0.75rem;
&:last-child {
margin-bottom: 0;
}
}
}
label {
font-size: 0.9rem;
font-weight: normal;
}
& > * {
margin-right: 1rem;
&:last-child {
margin-right: 0;
}
}
}
.paragraph-description {
display: flex;
flex-direction: row;
align-items: center;
color: hsl(var(--text-secondary));
width: 100%;
height: max-content;
font-size: 0.9rem;
margin: 0.75rem 0 1.25rem;
svg {
margin-right: 0.5rem;
flex-shrink: 0;
}
}
.paragraph-space {
width: 100%;
height: 0.25rem;
}
.paragraph-content {
transition: 1.5s ease-in-out;
}
.paragraph-footer {
display: flex;
flex-direction: row;
margin-top: 1rem;
& > * {
margin-right: 0.5rem;
&:last-child {
margin-right: 0;
}
}
}
&:hover {
border-color: hsl(var(--border-hover));
.paragraph-title {
color: hsl(var(--text));
&:before {
background: hsl(var(--text));
}
}
}
&.config-paragraph {
.paragraph-content {
input, button {
margin-left: auto;
}
}
}
}
.number-input {
transition: 0.25s;
transition-property: border-color;
}
.avatar {
display: flex;
border-radius: var(--radius);
text-align: center;
p {
margin: auto;
color: hsl(var(--text-dark));
}
}
.text-secondary {
color: hsl(var(--text-secondary)) !important;
}
.chart-tooltip,
.recharts-tooltip-wrapper > .rounded-tremor-default.text-tremor-default.border {
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
animation: fadeIn 0.5s;
position: absolute;
z-index: 64;
}
.border-input:focus {
border-color: hsl(var(--border));
}
.animate-fade-in {
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
animation: fadeIn 0.5s forwards;
}
.text-common {
color: hsl(var(--text)) !important;
}
.text-md {
font-size: 0.95rem;
}
.error-border {
border-color: hsl(var(--destructive)) !important;
}
.button-wrapper.is-loading {
.loading-hidden {
display: none;
}
}
================================================
FILE: app/src/components/Avatar.tsx
================================================
import { deeptrainApiEndpoint, useDeeptrain } from "@/conf/env.ts";
import { ImgHTMLAttributes, useMemo, useState, useEffect } from "react";
import { cn } from "@/components/ui/lib/utils.ts";
import { getUserInfo, UserInfo, initialUserInfo } from "@/api/auth.ts";
import md5 from "crypto-js/md5";
import { getConfig } from "@/admin/api/system";
import { useSelector, useDispatch } from "react-redux";
import { setAvatar } from "@/store/avatar";
export interface AvatarProps extends ImgHTMLAttributes {
username: string;
}
async function checkGravatar(
gravatar_endpoint: string,
email: string,
): Promise {
if (!email || email === "root@example.com") {
return false;
}
const trimmedEmail = email.trim().toLowerCase();
const hash = md5(trimmedEmail).toString();
const uri = `${gravatar_endpoint}/avatar/${hash}?d=404`;
try {
const response = await fetch(uri);
if (response.ok) {
return true;
}
console.info("[avatar] gravatar not found:", trimmedEmail);
return false;
} catch (error) {
console.error("[avatar] request failed:", error);
return false;
}
}
function Avatar({ username, ...props }: AvatarProps) {
const dispatch = useDispatch();
const cachedAvatarBlob = useSelector(
(state: any) => state.avatar.avatars[username],
);
const [userInfo, setUserInfo] = useState(initialUserInfo);
const [hasAvatar, setHasAvatar] = useState(false);
const [gravatar_endpoint, setGravatarEndpoint] = useState("");
useEffect(() => {
getUserInfo().then((info) => setUserInfo(info?.data ?? initialUserInfo));
}, []);
useEffect(() => {
getConfig().then((config) => {
if (
config.data.general.gravatar === undefined ||
config.data.general.gravatar === ""
) {
setGravatarEndpoint("");
return;
}
setGravatarEndpoint(config.data.general.gravatar);
});
}, []);
useEffect(() => {
if (cachedAvatarBlob !== null) {
setHasAvatar(true);
return;
}
checkGravatar(gravatar_endpoint, userInfo.email).then((hasAvatar) => {
setHasAvatar(hasAvatar);
if (hasAvatar) {
const avatarUrl = getGravatarUrl(userInfo.email);
fetch(avatarUrl)
.then((response) => response.blob())
.then((blob) => {
dispatch(setAvatar({ username, blob }));
});
}
});
}, [gravatar_endpoint, userInfo]);
const code = useMemo(
() => (username?.length > 0 ? username[0].toUpperCase() : "A"),
[username],
);
const background = useMemo(() => {
const colors = [
"bg-gradient-to-br from-red-500 to-orange-500",
"bg-gradient-to-br from-yellow-500 to-green-500",
"bg-gradient-to-br from-green-500 to-teal-500",
"bg-gradient-to-br from-indigo-500 to-purple-500",
"bg-gradient-to-br from-purple-500 to-pink-500",
"bg-gradient-to-br from-sky-500 to-blue-500",
"bg-gradient-to-br from-pink-500 to-rose-500",
];
const index = code.charCodeAt(0) % colors.length;
return colors[index];
}, [username]);
const getGravatarUrl = (email: string | undefined) => {
if (!email) return "";
const trimmedEmail = email.trim().toLowerCase();
const hash = md5(trimmedEmail).toString();
if (!gravatar_endpoint) return "";
return `${gravatar_endpoint}/avatar/${hash}?d=identicon`;
};
const avatarSrc =
useDeeptrain && username.length > 0
? `${deeptrainApiEndpoint}/avatar/${username}`
: hasAvatar && cachedAvatarBlob
? URL.createObjectURL(cachedAvatarBlob)
: hasAvatar && userInfo.email
? getGravatarUrl(userInfo.email)
: "";
return avatarSrc ? (
) : (
);
}
export default Avatar;
================================================
FILE: app/src/components/EditorProvider.tsx
================================================
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./ui/dialog.tsx";
import { Maximize, Image, MenuSquare, PanelRight, Eraser } from "lucide-react";
import { useTranslation } from "react-i18next";
import "@/assets/common/editor.less";
import { Textarea } from "./ui/textarea.tsx";
import Markdown from "./Markdown.tsx";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Toggle } from "./ui/toggle.tsx";
import { mobile } from "@/utils/device.ts";
import { Button } from "./ui/button.tsx";
import { ChatAction } from "@/components/home/assemblies/ChatAction.tsx";
import { cn } from "@/components/ui/lib/utils.ts";
type RichEditorProps = {
value: string;
onChange: (value: string) => void;
maxLength?: number;
formatter?: (value: string) => string;
isInvalid?: (value: string) => boolean;
title?: string;
open?: boolean;
setOpen?: (open: boolean) => void;
children?: React.ReactNode;
submittable?: boolean;
onSubmit?: (value: string) => void;
closeOnSubmit?: boolean;
};
function RichEditor({
value,
onChange,
maxLength,
formatter,
submittable,
isInvalid,
onSubmit,
setOpen,
closeOnSubmit,
}: RichEditorProps) {
const { t } = useTranslation();
const input = useRef(null);
const [openPreview, setOpenPreview] = useState(!mobile);
const [openInput, setOpenInput] = useState(true);
const formattedValue = useMemo(() => {
return formatter ? formatter(value) : value;
}, [value, formatter]);
const invalid = useMemo(() => {
return isInvalid ? isInvalid(value) : false;
}, [value, isInvalid]);
const handler = () => {
if (!input.current) return;
const target = input.current as HTMLElement;
const preview = target.parentElement?.querySelector(
".editor-preview",
) as HTMLElement | null;
if (!preview) {
setTimeout(handler, 100);
return;
}
const listener = () => {
preview.style.height = `${target.clientHeight}px`;
};
target.addEventListener("transitionstart", listener);
setInterval(listener, 250);
target.addEventListener("scroll", () => {
preview.scrollTop = target.scrollTop;
});
preview.style.height = `${target.clientHeight}px`;
if (openInput) target.focus();
};
useEffect(handler, [input]);
return (
value && onChange("")}
>
{
setOpenPreview(false);
setOpenInput(true);
}}
>
{
setOpenPreview(true);
setOpenInput(true);
}}
>
{
setOpenPreview(true);
setOpenInput(false);
}}
>
{submittable && (
setOpen?.(false)}
>
{t("cancel")}
{
onSubmit?.(value);
(closeOnSubmit ?? true) && setOpen?.(false);
}}
>
{t("submit")}
)}
);
}
function EditorProvider(props: RichEditorProps) {
const { t } = useTranslation();
return (
<>
{!props.setOpen && (
{props.children ?? (
)}
)}
{props.title ?? t("edit")}
>
);
}
export default EditorProvider;
export function JSONEditorProvider({ ...props }: RichEditorProps) {
return (
`\`\`\`json\n${value}\n\`\`\``}
isInvalid={(value) => {
try {
JSON.parse(value);
return false;
} catch (e) {
return true;
}
}}
/>
);
}
================================================
FILE: app/src/components/Emoji.tsx
================================================
import { cn } from "@/components/ui/lib/utils.ts";
export function getEmojiSource(emoji: string): string {
return `https://registry.npmmirror.com/@lobehub/fluent-emoji-3d/latest/files/assets/${emoji}.webp`;
}
type EmojiProps = {
emoji: string;
className?: string;
};
function Emoji({ emoji, className }: EmojiProps) {
return (
);
}
export default Emoji;
================================================
FILE: app/src/components/ErrorBoundary.tsx
================================================
import React from "react";
import { AlertCircle, Download } from "lucide-react";
import { withTranslation, WithTranslation } from "react-i18next";
import { version } from "@/conf/bootstrap.ts";
import { getMemoryPerformance } from "@/utils/app.ts";
import { Button } from "@/components/ui/button.tsx";
import { saveAsFile } from "@/utils/dom.ts";
type ErrorBoundaryProps = { children: React.ReactNode } & WithTranslation;
class ErrorBoundary extends React.Component<
ErrorBoundaryProps,
{ errorCaught: Error | null }
> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { errorCaught: null };
}
static getDerivedStateFromError(error: Error) {
return { errorCaught: error };
}
render() {
const { t } = this.props;
const ua = navigator.userAgent || "unknown";
const memory = getMemoryPerformance();
const time = new Date().toLocaleString();
const stamp = new Date().getTime();
const path = window.location.pathname;
const message = `Raised-Path: ${path}\nApp-Version: ${version}\nMemory-Usage: ${
!isNaN(memory) ? memory.toFixed(2) + " MB" : "unknown"
}\nLocale-Time: ${time}\nError-Message: ${
this.state.errorCaught?.message || "unknown"
}\nUser-Agent: ${ua}\nStack-Trace: ${
this.state.errorCaught?.stack || "unknown"
}`;
return this.state.errorCaught ? (
{t("fatal")}
Raised-Path: {path}
App-Version: {version}
Memory-Usage:{" "}
{!isNaN(memory) ? memory.toFixed(2) + " MB" : "unknown"}
Locale-Time: {time}
Error-Message: {this.state.errorCaught.message}
User-Agent: {ua}
saveAsFile(`error-${stamp}.log`, message)}>
{t("download-fatal-log")}
) : (
this.props.children
);
}
}
export default withTranslation()(ErrorBoundary);
================================================
FILE: app/src/components/FileProvider.tsx
================================================
import { useEffect, useMemo, useReducer, useRef, useState } from "react";
import {
Loader2,
Paperclip,
X,
FileIcon,
FileTextIcon,
FileImageIcon,
FileVideoIcon,
FileAudioIcon,
FileSpreadsheetIcon,
FileArchiveIcon,
FileCodeIcon,
FileJsonIcon,
FileVideo2Icon,
FileDigitIcon,
AlarmClock,
} from "lucide-react";
import "@/assets/common/file.less";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./ui/dialog";
import { useTranslation } from "react-i18next";
import { useDraggableInput } from "@/utils/dom.ts";
import { FileObject, FileArray, quickBlobParser } from "@/api/file.ts";
import { useSelector } from "react-redux";
import { getModelFromId, isHighContextModel } from "@/conf/model.ts";
import { selectModel, selectSupportModels } from "@/store/chat.ts";
import { ChatAction } from "@/components/home/assemblies/ChatAction.tsx";
import { blobEvent } from "@/events/blob.ts";
import { isB64Image } from "@/utils/base.ts";
import { toast } from "sonner";
import { Badge } from "./ui/badge.tsx";
import { AnimatePresence, motion } from "framer-motion";
import { cn } from "./ui/lib/utils.ts";
import { Progress } from "./ui/progress.tsx";
const MaxFileSize = 1024 * 1024 * 100; // 100MB File Size Limit
const MaxPromptSize = 10 * 1024; // 10KB Prompt Size Limit (to avoid token overflow)
type FileTask = {
id: number;
file: File;
progress: number;
};
type FileTaskState = {
tasks: FileTask[];
};
function fileTaskReducer(state: FileTaskState, action: any): FileTaskState {
switch (action.type) {
case "add":
return { ...state, tasks: [...state.tasks, action.payload] };
case "remove":
return {
...state,
tasks: state.tasks.filter((task) => task.id !== action.payload),
};
case "update-progress":
return {
...state,
tasks: state.tasks.map((task) =>
task.id === action.payload.id
? { ...task, progress: action.payload.progress }
: task,
),
};
default:
return state;
}
}
type FileProviderProps = {
files: FileArray;
dispatch: (action: Record) => void;
};
function FileProvider({ files, dispatch }: FileProviderProps) {
const { t } = useTranslation();
const model = useSelector(selectModel);
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [tasks, taskDispatch] = useReducer(fileTaskReducer, {
tasks: [],
} as FileTaskState);
const supportModels = useSelector(selectSupportModels);
useEffect(() => {
blobEvent.bind(async (file: File | File[]) => {
setOpen?.(true);
await triggerFile(Array.isArray(file) ? file : [file]);
});
}, []);
const triggerFile = async (files: (File | null)[]) => {
setLoading(true);
for (const file of files) {
if (!file) continue;
if (file.size > MaxFileSize) {
toast.error(t("file.over-size"), {
description: t("file.over-size-prompt", {
size: (MaxFileSize / 1024 / 1024).toFixed(),
}),
});
} else {
const id = Date.now();
taskDispatch({
type: "add",
payload: { id, file, progress: 0 },
});
const info = getModelFromId(supportModels, model);
const task = quickBlobParser(
file,
info ?? {
id: model,
ocr_model: false,
vision_model: false,
reverse_model: false,
},
(progress) => {
console.debug(
`[parser] task ${id} progress: ${progress.toFixed(2)}%`,
);
taskDispatch({
type: "update-progress",
payload: { id, progress },
});
},
);
toast.promise(task, {
loading: t("file.uploading-prompt"),
success: (content: string) => {
addFile({ name: file.name, content, size: file.size });
taskDispatch({
type: "remove",
payload: id,
});
return t("file.parse-success-prompt", { file: file.name });
},
error: (error: Error) => {
taskDispatch({
type: "remove",
payload: id,
});
return t("file.parse-error-prompt", { reason: error.message });
},
});
}
}
setLoading(false);
};
function addFile(file: FileObject) {
console.debug(
`[file] new file was added (filename: ${file.name}, size: ${file.size}, prompt: ${file.content.length})`,
);
if (
file.content.length > MaxPromptSize &&
!isHighContextModel(supportModels, model) &&
!isB64Image(file.content)
) {
file.content = file.content.slice(0, MaxPromptSize);
toast(t("file.max-length"), {
description: t("file.max-length-prompt"),
});
}
dispatch({ type: "add", payload: file });
}
function removeFile(index: number) {
dispatch({ type: "remove", payload: index });
}
return (
{t("file.file")}
{files.length}
{tasks.tasks.map((task, index) => (
))}
);
}
type FileTaskItemProps = {
task: FileTask;
};
function FileTaskItem({ task }: FileTaskItemProps) {
return (
{task.file.name}
{task.progress.toFixed()}%
);
}
type FileBadgeProps = {
name: string;
};
function getFileExtension(name: string) {
return name.split(".").pop()?.toLowerCase() || "";
}
function getFileIcon(name: string) {
const extension = getFileExtension(name);
switch (extension) {
case "pdf":
return FileTextIcon;
case "doc":
case "docx":
case "txt":
return FileDigitIcon;
case "xls":
case "xlsx":
case "csv":
return FileSpreadsheetIcon;
case "ppt":
case "pptx":
return FileVideo2Icon;
case "jpg":
case "jpeg":
case "png":
case "gif":
case "svg":
return FileImageIcon;
case "mp4":
case "avi":
case "mov":
return FileVideoIcon;
case "mp3":
case "wav":
return FileAudioIcon;
case "zip":
case "rar":
case "7z":
return FileArchiveIcon;
case "js":
case "ts":
case "py":
case "java":
case "cpp":
case "c":
case "h":
case "rs":
case "swift":
case "kt":
case "ktm":
case "php":
case "rb":
case "sh":
case "html":
case "css":
case "scss":
case "less":
case "sass":
case "styl":
case "vue":
case "svelte":
case "astro":
case "tsx":
case "jsx":
return FileCodeIcon;
case "json":
case "xml":
case "jsonl":
case "yaml":
case "yml":
case "toml":
case "ini":
case "cfg":
case "conf":
return FileJsonIcon;
default:
return FileIcon;
}
}
function FileIconObject({ name }: FileBadgeProps) {
const IconComponent = useMemo(() => getFileIcon(name), [name]);
return (
);
}
function FileBadge({ name }: FileBadgeProps) {
const extension = getFileExtension(name);
return (
{extension.toUpperCase()}
);
}
type FileListProps = {
value: FileArray;
removeFile: (index: number) => void;
};
function FileList({ value, removeFile }: FileListProps) {
if (value.length === 0) return null;
const listVariants = {
hidden: { opacity: 0, height: 0 },
visible: { opacity: 1, height: "auto" },
};
const itemVariants = {
hidden: { opacity: 0, y: 10 },
visible: { opacity: 1, y: 0 },
};
return (
{value.map((file, index) => (
{file.name}
{((file.size || file.content.length) / 1024).toFixed(2)}KB
{
e.stopPropagation();
removeFile(index);
}}
>
))}
);
}
type FileInputProps = {
id: string;
loading: boolean;
className?: string;
handleEvent: (files: (File | null)[]) => void;
};
function FileInput({ id, loading, className, handleEvent }: FileInputProps) {
const { t } = useTranslation();
const ref = useRef(null);
useEffect(() => {
return useDraggableInput(window.document.body, handleEvent);
}, []);
return (
<>
{loading && }
{t("file.drop")}
{[
{ icon: FileIcon, text: "Text" },
{ icon: FileVideo2Icon, text: "PPT" },
{ icon: FileDigitIcon, text: "Word" },
{ icon: FileTextIcon, text: "PDF" },
{ icon: FileSpreadsheetIcon, text: "Excel" },
{ icon: FileImageIcon, text: "Image" },
{ icon: FileAudioIcon, text: "Audio" },
{ icon: FileCodeIcon, text: "Code" },
{ icon: FileJsonIcon, text: "Data" },
].map((item, index) => (
{item.text}
))}
handleEvent(Array.from(e.target?.files || []))}
accept="*"
style={{ display: "none" }}
multiple={true}
// on transfer file
onPaste={(e) => {
const items = e.clipboardData.items;
const files = Array.from(items).filter(
(item) => item.kind === "file",
);
handleEvent(files.map((file) => file.getAsFile()));
}}
/>
>
);
}
export default FileProvider;
================================================
FILE: app/src/components/FileViewer.tsx
================================================
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./ui/dialog";
import { useTranslation } from "react-i18next";
import React from "react";
import { Heading2, Paperclip, Text } from "lucide-react";
import { Textarea } from "@/components/ui/textarea.tsx";
import { CodeMarkdown } from "@/components/Markdown.tsx";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group.tsx";
type FileViewerProps = {
filename: string;
content: string;
children: React.ReactNode;
asChild?: boolean;
};
enum viewerType {
Text = "text",
Image = "image",
}
function FileViewer({ filename, content, children, asChild }: FileViewerProps) {
const { t } = useTranslation();
const [renderedType, setRenderedType] = React.useState(viewerType.Text);
return (
{children}
{filename ?? t("file.file")}
setRenderedType(viewerType.Text)}
>
setRenderedType(viewerType.Image)}
>
{renderedType === viewerType.Text ? (
) : (
{content}
)}
);
}
export default FileViewer;
================================================
FILE: app/src/components/I18nProvider.tsx
================================================
import { Button } from "./ui/button.tsx";
import { Languages } from "lucide-react";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "./ui/dropdown-menu.tsx";
import { langsProps, setLanguage } from "@/i18n.ts";
import { useTranslation } from "react-i18next";
function I18nProvider() {
const { i18n } = useTranslation();
return (
{Object.entries(langsProps).map(([key, value]) => (
setLanguage(i18n, key)}
>
{value}
))}
);
}
export default I18nProvider;
================================================
FILE: app/src/components/Loader.tsx
================================================
import "@/assets/common/loader.less";
type LoaderProps = {
className?: string;
prompt?: string;
};
function Loader({ className, prompt }: LoaderProps) {
return (
);
}
export default Loader;
================================================
FILE: app/src/components/MCPResultDebug.tsx
================================================
import { useTranslation } from "react-i18next";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useEffect, useRef, useState } from "react";
interface MCPResultDebugProps {
toolCall: {
function: {
arguments: string;
};
result?: string;
error?: string;
};
}
export function MCPResultDebug({ toolCall }: MCPResultDebugProps): JSX.Element {
const { t } = useTranslation();
const hasResult = !!toolCall.result;
const hasError = !!toolCall.error;
const defaultTab = hasResult ? "result" : hasError ? "error" : "arguments";
const formatContent = (content: string): string => {
try {
const parsed = JSON.parse(content);
return JSON.stringify(parsed, null, 2);
} catch {
return content;
}
};
const formattedArguments = formatContent(toolCall.function.arguments);
const formattedResult = toolCall.result ? formatContent(toolCall.result) : '';
const formattedError = toolCall.error ? formatContent(toolCall.error) : '';
const [contentHeights, setContentHeights] = useState<{
arguments: number;
result: number;
error: number;
}>({ arguments: 0, result: 0, error: 0 });
const argumentsRef = useRef(null);
const resultRef = useRef(null);
const errorRef = useRef(null);
useEffect(() => {
const measureHeight = () => {
const newHeights = {
arguments: argumentsRef.current?.scrollHeight || 0,
result: resultRef.current?.scrollHeight || 0,
error: errorRef.current?.scrollHeight || 0,
};
setContentHeights(newHeights);
};
const timeout = setTimeout(measureHeight, 100);
return () => clearTimeout(timeout);
}, [formattedArguments, formattedResult, formattedError]);
const SCROLL_THRESHOLD = 200;
const ContentWrapper = ({
children,
shouldScroll,
className
}: {
children: React.ReactNode;
shouldScroll: boolean;
className?: string;
}) => {
if (shouldScroll) {
return (
{children}
);
}
return {children}
;
};
return (
{t("plugin.mcp.raw-arguments")}
{hasResult && (
{t("plugin.mcp.result")}
)}
{hasError && (
{t("plugin.mcp.error")}
)}
SCROLL_THRESHOLD}>
{formattedArguments}
{hasResult && (
SCROLL_THRESHOLD}>
{formattedResult}
)}
{hasError && (
SCROLL_THRESHOLD}>
{formattedError}
)}
);
}
================================================
FILE: app/src/components/MCPResultPanel.tsx
================================================
import {
CheckCircle,
XCircle,
Loader2,
Copy,
Bug,
BugOff,
Edit,
Check
} from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { MCPResultDebug } from "./MCPResultDebug";
import { useClipboard } from "@/utils/dom";
interface ToolArgumentEditorProps {
paramKey: string;
paramValue: unknown;
onValueChange: (key: string, value: unknown) => void;
}
function ToolArgumentEditor({
paramKey,
paramValue,
onValueChange
}: ToolArgumentEditorProps): JSX.Element {
const { t } = useTranslation();
const copy = useClipboard();
const [isEditing, setIsEditing] = useState(false);
const [editValue, setEditValue] = useState(String(paramValue || ''));
const handleSave = () => {
try {
const parsedValue = editValue.startsWith('{') || editValue.startsWith('[')
? JSON.parse(editValue)
: editValue;
onValueChange(paramKey, parsedValue);
} catch {
onValueChange(paramKey, editValue);
}
setIsEditing(false);
};
const handleCopy = async () => {
await copy(String(paramValue));
};
const displayValue = typeof paramValue === 'object'
? JSON.stringify(paramValue, null, 2)
: String(paramValue);
return (
{paramKey}:
{isEditing ? (
{
if (isEditing) {
handleSave();
} else {
setEditValue(displayValue);
setIsEditing(true);
}
}}
className="p-0.5 text-muted-foreground hover:text-foreground transition-colors"
title={isEditing ? t("plugin.mcp.save") : t("plugin.mcp.edit")}
>
{isEditing ? : }
);
}
interface SingleToolCallPanelProps {
toolCall: {
index: number;
type: string;
id: string;
function: {
name: string;
arguments: string;
};
status?: "start" | "executing" | "success" | "error";
result?: string;
error?: string;
};
pluginName?: string;
}
export function SingleToolCallPanel({
toolCall,
pluginName = "MCP"
}: SingleToolCallPanelProps): JSX.Element {
const { t } = useTranslation();
const [showDebug, setShowDebug] = useState(false);
const getStatusIcon = () => {
switch (toolCall.status) {
case "start":
case "executing":
return ;
case "success":
return ;
case "error":
return ;
default:
return ;
}
};
const getStatusDescription = () => {
switch (toolCall.status) {
case "start":
return t("plugin.mcp.status-prepare");
case "executing":
return t("plugin.mcp.status-executing");
case "success":
return t("plugin.mcp.status-success");
case "error":
return t("plugin.mcp.status-error");
default:
return t("plugin.mcp.status-success");
}
};
const argumentsObj = (() => {
try {
return JSON.parse(toolCall.function.arguments);
} catch {
return { value: toolCall.function.arguments };
}
})();
return (
{pluginName} / {toolCall.function.name}
{getStatusIcon()}
{getStatusDescription()}
setShowDebug(!showDebug)}
className="px-2 py-1 text-xs bg-muted hover:bg-muted/80 border rounded transition-colors flex items-center gap-1"
title={showDebug ? t("plugin.mcp.hide-debug") : t("plugin.mcp.show-debug")}
>
{showDebug ? : }
DEBUG
{t("plugin.mcp.tool-arguments")}
{Object.keys(argumentsObj).length > 0 ? (
{Object.entries(argumentsObj).map(([key, value]) => (
{}}
/>
))}
) : (
{t("plugin.mcp.no-arguments-needed")}
)}
{showDebug && (
)}
);
}
================================================
FILE: app/src/components/Markdown.tsx
================================================
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import remarkBreaks from "remark-breaks";
import rehypeKatex from "rehype-katex";
import rehypeRaw from "rehype-raw";
import "@/assets/markdown/all.less";
import { useEffect, useMemo } from "react";
import { cn } from "@/components/ui/lib/utils.ts";
import Label from "@/components/markdown/Label.tsx";
import Link from "@/components/markdown/Link.tsx";
import Code, { CodeProps } from "@/components/markdown/Code.tsx";
import Image from "@/components/markdown/Image.tsx";
import Video from "@/components/markdown/Video.tsx";
type MarkdownProps = {
children: string;
className?: string;
acceptHtml?: boolean;
codeStyle?: string;
loading?: boolean;
};
function MarkdownContent({
children,
className,
acceptHtml,
codeStyle,
loading,
}: MarkdownProps) {
useEffect(() => {
document.querySelectorAll(".file-instance").forEach((el) => {
const parent = el.parentElement as HTMLElement;
if (!parent.classList.contains("file-block"))
parent.classList.add("file-block");
});
}, [children]);
const rehypePlugins = useMemo(() => {
const plugins = [rehypeKatex as any];
return acceptHtml ? [...plugins, rehypeRaw] : plugins;
}, [acceptHtml]);
const components = useMemo(() => {
return {
p: Label,
a: Link,
img: (props: React.ImgHTMLAttributes) => {
if (props.alt === "video") {
return (
);
}
return ;
},
code: (props: CodeProps) => (
),
};
}, [codeStyle]);
return (
);
}
function Markdown({
children,
acceptHtml,
codeStyle,
className,
loading,
}: MarkdownProps) {
// memoize the component
return useMemo(
() => (
),
[children, acceptHtml, codeStyle, className, loading],
);
}
type CodeMarkdownProps = MarkdownProps & {
filename: string;
language?: string;
};
export function CodeMarkdown({
filename,
language,
...props
}: CodeMarkdownProps) {
const suffix =
language ?? (filename.includes(".") ? filename.split(".").pop() : "");
const children = useMemo(() => {
const content = props.children.toString();
return `\`\`\`${suffix}\n${content}\n\`\`\``;
}, [props.children]);
return {children} ;
}
export default Markdown;
================================================
FILE: app/src/components/Message.tsx
================================================
import { Message, UserRole } from "@/api/types.tsx";
import Markdown from "@/components/Markdown.tsx";
import {
CalendarCheck2,
CircleSlash,
Cloud,
CloudCog,
Copy,
File,
Loader2,
SquareMousePointer,
PencilLine,
Power,
RotateCcw,
Trash,
} from "lucide-react";
import { filterMessage } from "@/utils/processor.ts";
import {
copyClipboard,
isContainDom,
saveAsFile,
} from "@/utils/dom.ts";
import { useTranslation } from "react-i18next";
import React, { Ref, useRef, useState } from "react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu.tsx";
import { cn } from "@/components/ui/lib/utils.ts";
import EditorProvider from "@/components/EditorProvider.tsx";
import Avatar from "@/components/Avatar.tsx";
import { useSelector } from "react-redux";
import { selectUsername } from "@/store/auth.ts";
import { appLogo } from "@/conf/env.ts";
import { motion } from "framer-motion";
import { ThinkContent } from "@/components/ThinkContent";
type MessageProps = {
index: number;
message: Message;
end?: boolean;
username?: string;
onEvent?: (event: string, index?: number, message?: string) => void;
ref?: Ref;
sharing?: boolean;
selected?: boolean;
onFocus?: (event: React.MouseEvent) => void;
onFocusLeave?: (event: React.MouseEvent) => void;
};
function MessageSegment(props: MessageProps) {
const ref = useRef(null);
const { message } = props;
return (
{
try {
if (isContainDom(ref.current, event.relatedTarget as HTMLElement))
return;
props.onFocusLeave && props.onFocusLeave(event);
} catch (e) {
console.debug(`[message] cannot leave focus: ${e}`);
}
}}
>
);
}
type MessageQuotaProps = {
message: Message;
};
function MessageQuota({ message }: MessageQuotaProps) {
const [detail, setDetail] = useState(false);
if (message.role === UserRole) return null;
return (
message.quota &&
message.quota !== 0 && (
setDetail(!detail)}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, ease: "easeOut" }}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{message.plan ? (
) : detail ? (
) : (
)}
{(message.quota < 0 ? 0 : message.quota).toFixed(detail ? 6 : 2)}
)
);
}
type MessageMenuProps = {
children?: React.ReactNode;
message: Message;
end?: boolean;
index: number;
onEvent?: (event: string, index?: number, message?: string) => void;
editedMessage?: string;
setEditedMessage: (message: string) => void;
setOpen: (open: boolean) => void;
align?: "start" | "end";
};
function MessageMenu({
children,
align,
message,
end,
index,
onEvent,
editedMessage,
setEditedMessage,
setOpen,
}: MessageMenuProps) {
const { t } = useTranslation();
const isAssistant = message.role === "assistant";
const notInOutput = message.end !== false;
const disableDelete = isAssistant && end && !notInOutput;
const [dropdown, setDropdown] = useState(false);
return (
{children}
{isAssistant && end ? (
{
onEvent && onEvent(message.end !== false ? "restart" : "stop");
setDropdown(false);
}}
>
{notInOutput ? (
<>
{t("message.restart")}
>
) : (
<>
{t("message.stop")}
>
)}
) : (
notInOutput && (
{
onEvent && onEvent("restart");
setDropdown(false);
}}
>
{t("message.restart")}
)
)}
copyClipboard(filterMessage(message.content))}
>
{t("message.copy")}
{
const input = document.getElementById("input") as HTMLInputElement;
if (input) {
input.value = filterMessage(message.content);
input.focus();
}
}}
>
{t("message.use")}
{
editedMessage?.length === 0 && setEditedMessage(message.content);
setOpen(true);
}}
>
{t("message.edit")}
onEvent && onEvent("remove", index)}
>
{t("message.remove")}
saveAsFile(
`message-${message.role}.txt`,
filterMessage(message.content),
)
}
>
{t("message.save")}
);
}
function MessageContent({
message,
end,
index,
onEvent,
selected,
username,
}: MessageProps) {
const isUser = message.role === "user";
const hasContent = message.content.length > 0;
const isAssistant = message.role === "assistant";
const isOutput = message.end === false;
const user = useSelector(selectUsername);
const [open, setOpen] = useState(false);
const [editedMessage, setEditedMessage] = useState("");
// parse think content
const parseThinkContent = (content: string) => {
// check if there is a start tag
const startMatch = content.match(/\n?(.*?)(?:<\/think>|$)/s);
if (startMatch) {
const thinkContent = startMatch[1];
// if there is an end tag, remove the whole matching part;
// if not, keep the remaining content
const hasEndTag = content.includes(' ');
const restContent = hasEndTag ?
content.replace(startMatch[0], "").trim() :
content.substring(content.indexOf('') + 7).trim();
return {
thinkContent,
restContent: hasEndTag ? restContent : '',
isComplete: hasEndTag
};
}
return null;
};
const parsedContent = message.content.length ? parseThinkContent(message.content) : null;
return (
onEvent && onEvent("edit", index, value)}
open={open}
setOpen={setOpen}
value={editedMessage ?? ""}
onChange={setEditedMessage}
/>
{!selected ? (
isUser ? (
) : (
)
) : (
)}
{hasContent ? (
<>
{parsedContent ? (
<>
{parsedContent.restContent && (
)}
>
) : (
)}
>
) : message.end === true ? (
) : (
)}
{isAssistant && hasContent && isOutput && (
)}
);
}
export default MessageSegment;
================================================
FILE: app/src/components/ModelAvatar.tsx
================================================
import { isUrl } from "@/utils/base.ts";
import { Model } from "@/api/types.tsx";
import {
Claude,
Gemini,
Gemma,
OpenAI,
Spark,
Qwen,
Baichuan,
ByteDance,
Meta,
Copilot,
Hunyuan,
Midjourney,
Stability,
Moonshot,
LLaVA,
DeepSeek,
Grok,
Minimax,
Mistral,
Dalle,
Rwkv,
Cloudflare,
Cohere,
Fireworks,
Groq,
OpenRouter,
Perplexity,
GithubCopilot,
Suno,
Qingyan,
IconAvatarProps,
Azure,
Coze,
Dify
} from "@lobehub/icons";
import React from "react";
import { cn } from "@/components/ui/lib/utils.ts";
type ModelAvatarProps = {
model: Model | { id: string; name: string; avatar?: string };
className?: string;
size?: number;
};
const builtinAvatars: Record> = {
openai: OpenAI.Avatar,
"gpt-3.5": OpenAI.Avatar,
"gpt-4": OpenAI.Avatar,
dalle: Dalle.Avatar,
"dall-e": Dalle.Avatar,
azure: Azure.Avatar,
claude: Claude.Avatar,
anthropic: Claude.Avatar,
gemini: Gemini.Avatar,
palm: Gemma.Avatar,
gemma: Gemma.Avatar,
"chat-bison": Gemma.Avatar, // "chat-bision" is a typo, but we need to keep it for compatibility
google: Gemini.Avatar,
glm: Qingyan.Avatar,
zhipu: Qingyan.Avatar,
spark: Spark.Avatar,
tongyi: Qwen.Avatar,
qwen: Qwen.Avatar,
baichuan: Baichuan.Avatar,
byte: ByteDance.Avatar,
bytedance: ByteDance.Avatar,
skylark: ByteDance.Avatar,
meta: Meta.Avatar,
llama: Meta.Avatar,
bing: Copilot.Avatar,
hunyuan: Hunyuan.Avatar,
midjourney: Midjourney.Avatar,
stability: Stability.Avatar,
"stable-diffusion": Stability.Avatar,
stablediffusion: Stability.Avatar,
sd: Stability.Avatar,
moonshot: Moonshot.Avatar,
kimi: Moonshot.Avatar,
llava: LLaVA.Avatar,
deepseek: DeepSeek.Avatar,
"deep-seek": DeepSeek.Avatar,
coze: Coze.Avatar,
dify: Dify.Avatar,
grok: Grok.Avatar,
minimax: Minimax.Avatar,
abab: Minimax.Avatar,
mistral: Mistral.Avatar,
rwkv: Rwkv.Avatar,
cf: Cloudflare.Combine,
cloudflare: Cloudflare.Combine,
command: Cohere.Avatar,
cohere: Cohere.Avatar,
firework: Fireworks.Avatar,
groq: Groq.Avatar,
router: OpenRouter.Avatar,
perplexity: Perplexity.Avatar,
copilot: GithubCopilot.Avatar,
suno: Suno.Avatar,
};
function getAvatarType(id: string): string | undefined {
if (id.includes("gpt-3.5")) return "gpt3";
if (id.includes("gpt-4") || id.includes("o1")) return "gpt4";
}
function ModelAvatar({ model, className, size }: ModelAvatarProps) {
const avatarSize = size ?? 42;
if (isUrl(model.avatar ?? "")) {
return (
);
}
// if key is include, return value (reactelement)
const id = model.id.toLowerCase();
const key = Object.keys(builtinAvatars).find((key) => id.includes(key));
const Avatar = key ? builtinAvatars[key] : OpenAI.Avatar;
return (
);
}
export default ModelAvatar;
export type ChannelTypeAvatarProps = {
type: string;
size?: number;
className?: string;
};
export function ChannelTypeAvatar({
type,
size,
className,
}: ChannelTypeAvatarProps) {
const key = Object.keys(builtinAvatars).find((key) => type.includes(key));
const Avatar = key ? builtinAvatars[key] : OpenAI.Avatar;
return ;
}
================================================
FILE: app/src/components/OperationAction.tsx
================================================
import React from "react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip.tsx";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover.tsx";
import { Button, type ButtonProps } from "@/components/ui/button.tsx";
import { cn } from "@/components/ui/lib/utils.ts";
type ActionProps = ButtonProps & {
tooltip?: string;
children: React.ReactNode;
onClick?: () => any;
native?: boolean;
variant?:
| "secondary"
| "default"
| "destructive"
| "outline"
| "ghost"
| "link"
| null
| undefined;
};
function OperationAction({
tooltip,
children,
onClick,
variant,
native,
className,
...props
}: ActionProps) {
return (
{variant === "destructive" ? (
{children}
{children}
{tooltip}
) : (
{children}
)}
{tooltip}
);
}
export default OperationAction;
================================================
FILE: app/src/components/Paragraph.tsx
================================================
import React from "react";
import { Info } from "lucide-react";
import { cn } from "@/components/ui/lib/utils.ts";
import Markdown from "@/components/Markdown.tsx";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion.tsx";
import { Badge } from "@/components/ui/badge.tsx";
export type ParagraphProps = {
isPro?: boolean;
title?: string;
children: React.ReactNode;
className?: string;
configParagraph?: boolean;
isCollapsed?: boolean;
};
function Paragraph({
title,
children,
className,
configParagraph,
isCollapsed,
isPro,
}: ParagraphProps) {
return (
{title ?? ""}
{isPro && (
Pro
)}
{children}
);
}
function ParagraphItem({
children,
className,
rowLayout,
}: {
children: React.ReactNode;
className?: string;
rowLayout?: boolean;
}) {
return (
{children}
);
}
type ParagraphDescriptionProps = {
children: string;
border?: boolean;
hideIcon?: boolean;
className?: string;
classNameMarkdown?: string;
};
export function ParagraphDescription({
children,
border,
hideIcon,
className,
classNameMarkdown,
}: ParagraphDescriptionProps) {
return (
{!hideIcon && }
);
}
export function ParagraphSpace() {
return
;
}
function ParagraphFooter({ children }: { children: React.ReactNode }) {
return {children}
;
}
export default Paragraph;
export { ParagraphItem, ParagraphFooter };
================================================
FILE: app/src/components/PopupDialog.tsx
================================================
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog.tsx";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button.tsx";
import { Input } from "@/components/ui/input.tsx";
import { useEffect, useMemo, useState } from "react";
import { NumberInput } from "@/components/ui/number-input.tsx";
import { Switch } from "@/components/ui/switch.tsx";
import { Alert, AlertDescription } from "./ui/alert";
import { AlertCircle } from "lucide-react";
import {
AlertDialog,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Calendar } from "@/components/ui/calendar.tsx";
import { Separator } from "@/components/ui/separator.tsx";
import { Combobox } from "@/components/ui/combo-box.tsx";
import { MultiCombobox } from "./ui/multi-combobox";
export enum popupTypes {
Text = "text",
Number = "number",
Switch = "switch",
Clock = "clock",
List = "list",
MultiList = "multi-list",
Empty = "empty",
}
type ParamProps = {
dataList?: string[];
dataListTranslated?: string;
};
export type PopupDialogProps = {
title: string;
description?: string;
name?: string;
placeholder?: string;
defaultValue?: string;
onValueChange?: (value: string) => string;
params?: ParamProps;
onSubmit?: (value: string) => Promise;
destructive?: boolean;
disabled?: boolean;
type?: popupTypes;
open: boolean;
setOpen: (open: boolean) => void;
cancelLabel?: string;
confirmLabel?: string;
componentProps?: any;
alert?: string;
};
type PopupFieldProps = PopupDialogProps & {
value: string;
setValue: (value: string) => void;
};
function PopupField({
type,
setValue,
onValueChange,
params,
value,
placeholder,
componentProps,
}: PopupFieldProps) {
switch (type) {
case popupTypes.Text:
return (
{
setValue(
onValueChange ? onValueChange(e.target.value) : e.target.value,
);
}}
value={value}
placeholder={placeholder}
{...componentProps}
/>
);
case popupTypes.Clock:
return setValue(v)} />;
case popupTypes.List:
return (
setValue(v)}
list={params?.dataList || []}
listTranslated={params?.dataListTranslated || ""}
/>
);
case popupTypes.MultiList:
return (
setValue(v.filter((i) => i.trim()).join(","))}
list={params?.dataList || []}
listTranslate={params?.dataListTranslated || ""}
placeholder={placeholder || value || ""}
/>
);
case popupTypes.Number:
return (
setValue(v.toString())}
placeholder={placeholder}
{...componentProps}
/>
);
case popupTypes.Switch:
return (
{
setValue(state.toString());
}}
{...componentProps}
/>
);
case popupTypes.Empty:
return null;
default:
return null;
}
}
function fixedZero(val: number) {
return val < 10 ? `0${val}` : val.toString();
}
function CalendarComp(props: {
value: string;
onValueChange: (v: string) => void;
}) {
const { value, onValueChange } = props;
const { t } = useTranslation();
const convertedDate = useMemo(() => {
const date = new Date(value.split(" ")[0] || "1970-01-01");
console.log(`[calendar] converted date:`, date);
return date;
}, [value]);
const onDateChange = (date: Date, overrideTime?: boolean) => {
const v = `${date.getFullYear()}-${fixedZero(
date.getMonth() + 1,
)}-${fixedZero(date.getDate())}`;
const t = !overrideTime
? value.split(" ")[1] || "00:00:00"
: `${fixedZero(date.getHours())}:${fixedZero(
date.getMinutes(),
)}:${fixedZero(date.getSeconds())}`;
console.log(`[calendar] clicked date: [${v} ${t}]`);
onValueChange(`${v} ${t}`);
};
const [month, setMonth] = useState(convertedDate);
useEffect(() => {
setMonth(convertedDate);
}, [convertedDate]);
return (
date && setMonth(date)}
selected={convertedDate}
onSelect={(date) => date && onDateChange(date)}
/>
onValueChange(e.target.value)}
placeholder={t("date.pick")}
className={`w-full text-center`}
/>
onDateChange(new Date("1970-01-01 00:00:00"), true)}
>
{t("date.clean")}
onDateChange(new Date(), true)}
>
{t("date.today")}
onDateChange(
new Date(convertedDate.setDate(convertedDate.getDate() + 1)),
)
}
>
{t("date.add-day")}
onDateChange(
new Date(convertedDate.setDate(convertedDate.getDate() - 1)),
)
}
>
{t("date.sub-day")}
onDateChange(
new Date(convertedDate.setMonth(convertedDate.getMonth() + 1)),
)
}
>
{t("date.add-month")}
onDateChange(
new Date(convertedDate.setMonth(convertedDate.getMonth() - 1)),
)
}
>
{t("date.sub-month")}
onDateChange(
new Date(
convertedDate.setFullYear(convertedDate.getFullYear() + 1),
),
)
}
>
{t("date.add-year")}
onDateChange(
new Date(
convertedDate.setFullYear(convertedDate.getFullYear() - 1),
),
)
}
>
{t("date.sub-year")}
);
}
function PopupDialog(props: PopupDialogProps) {
const {
title,
description,
name,
type,
defaultValue,
onSubmit,
open,
setOpen,
cancelLabel,
confirmLabel,
destructive,
disabled,
alert,
} = props;
const { t } = useTranslation();
const [value, setValue] = useState(defaultValue || "");
return (
{title}
{description}
{!(type === popupTypes.Empty) && (
)}
{alert && (
{alert}
)}
setOpen(false)}
>
{cancelLabel || t("cancel")}
{
if (!onSubmit) return;
const status: boolean = await onSubmit(value);
if (status) {
setOpen(false);
setValue(defaultValue || "");
}
}}
>
{confirmLabel || t("confirm")}
);
}
type PopupAlertDialogProps = {
title: string;
description?: string;
open: boolean;
setOpen: (open: boolean) => void;
cancelLabel?: string;
confirmLabel?: string;
destructive?: boolean;
disabled?: boolean;
onSubmit?: () => Promise;
};
export function PopupAlertDialog({
title,
description,
open,
setOpen,
cancelLabel,
confirmLabel,
destructive,
disabled,
onSubmit,
}: PopupAlertDialogProps) {
const { t } = useTranslation();
return (
{title}
{description && (
{description}
)}
{cancelLabel || t("cancel")}
{
if (!onSubmit) return;
const status: boolean = await onSubmit();
if (status) {
setOpen(false);
}
}}
>
{confirmLabel || t("confirm")}
);
}
export default PopupDialog;
================================================
FILE: app/src/components/ProjectLink.tsx
================================================
import { Button } from "./ui/button.tsx";
import { useConversationActions, useMessages } from "@/store/chat.ts";
import { MessageSquarePlus } from "lucide-react";
import Github from "@/components/ui/icons/Github.tsx";
import { openWindow } from "@/utils/device.ts";
function ProjectLink() {
const messages = useMessages();
const { toggle } = useConversationActions();
return messages.length > 0 ? (
await toggle(-1)}
>
) : (
openWindow("https://github.com/coaidev/coai")}
>
);
}
export default ProjectLink;
================================================
FILE: app/src/components/ReloadService.tsx
================================================
import { version } from "@/conf/bootstrap.ts";
import { useTranslation } from "react-i18next";
import { getMemory, setMemory } from "@/utils/memory.ts";
import { Badge } from "@/components/ui/badge.tsx";
import { toast } from "sonner";
function ReloadPrompt() {
const { t } = useTranslation();
const before = getMemory("version");
if (version.length === 0) {
return <>>;
}
if (before.length > 0 && before !== version) {
setMemory("version", version);
setTimeout(() => {
toast.success(t("service.update-success"), {
description: (
v{version}
{t("service.update-success-prompt")}
),
});
}, 2500);
console.debug(
`[service] service worker updated (from ${before} to ${version})`,
);
}
setMemory("version", version);
return <>>;
}
export default ReloadPrompt;
================================================
FILE: app/src/components/Require.tsx
================================================
import { useTranslation } from "react-i18next";
import { useMemo } from "react";
import { isEmailValid } from "@/utils/form.ts";
import { cn } from "@/components/ui/lib/utils.ts";
function Required() {
return * ;
}
export type LengthRangeRequiredProps = {
content: string;
min: number;
max: number;
hideOnEmpty?: boolean;
};
export function LengthRangeRequired({
content,
min,
max,
hideOnEmpty,
}: LengthRangeRequiredProps) {
const { t } = useTranslation();
const onDisplay = useMemo(() => {
if (hideOnEmpty && content.length === 0) return false;
return content.length < min || content.length > max;
}, [content, min, max, hideOnEmpty]);
return (
({t("auth.length-range", { min, max })})
);
}
export type SameRequiredProps = {
content: string;
compare: string;
hideOnEmpty?: boolean;
};
export function SameRequired({
content,
compare,
hideOnEmpty,
}: SameRequiredProps) {
const { t } = useTranslation();
const onDisplay = useMemo(() => {
if (hideOnEmpty && compare.length === 0) return false;
return content !== compare;
}, [content, compare, hideOnEmpty]);
return (
({t("auth.same-rule")})
);
}
export type EmailRequireProps = {
content: string;
hideOnEmpty?: boolean;
};
export function EmailRequire({ content, hideOnEmpty }: EmailRequireProps) {
const { t } = useTranslation();
const onDisplay = useMemo(() => {
if (hideOnEmpty && content.length === 0) return false;
return !isEmailValid(content);
}, [content, hideOnEmpty]);
return (
({t("auth.invalid-email")})
);
}
export default Required;
================================================
FILE: app/src/components/SelectGroup.tsx
================================================
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "./ui/select";
import { mobile } from "@/utils/device.ts";
import React, { useEffect, useState } from "react";
import { Badge } from "./ui/badge.tsx";
import { cn } from "@/components/ui/lib/utils.ts";
import Tips from "./Tips.tsx";
export type SingleSelectItemBadgeProps = {
variant: string;
name?: string;
icon?: React.ReactNode;
className?: string;
tooltip?: string;
};
export type SelectItemBadgeProps =
| SingleSelectItemBadgeProps
| SingleSelectItemBadgeProps[];
export type SelectItemProps = {
name: string;
value: string;
badge?: SelectItemBadgeProps;
tag?: any;
icon?: React.ReactNode;
};
type SelectGroupProps = {
current: SelectItemProps;
list: SelectItemProps[];
onChange?: (select: string) => void;
maxElements?: number;
className?: string;
classNameDesktop?: string;
classNameMobile?: string;
side?: "left" | "right" | "top" | "bottom";
selectGroupTop?: SelectItemProps;
selectGroupBottom?: SelectItemProps;
};
export function SingleGroupSelectItemBadge(props: SingleSelectItemBadgeProps) {
const Comp = (
{props.icon}
{props.name}
);
return props.tooltip ? {props.tooltip} : Comp;
}
function GroupSelectItemBadge(props: { data: SelectItemBadgeProps }) {
return Array.isArray(props.data) ? (
props.data.map((badge) => )
) : (
);
}
export function GroupSelectItem(props: SelectItemProps) {
return (
{props.icon &&
{props.icon}
}
{props.value}
{props.badge &&
}
);
}
function SelectGroupDesktop(props: SelectGroupProps) {
const max: number = props.maxElements || 5;
const range = props.list.length > max ? max : props.list.length;
const display = props.list.slice(0, range);
const hidden = props.list.slice(range);
return (
{display.map((select: SelectItemProps, idx: number) => (
props.onChange?.(select.name)}
className={`select-group-item ${
select == props.current ? "active" : ""
}`}
>
))}
{props.list.length > max && (
props.onChange?.(value)}
>
{hidden.includes(props.current) ? (
) : (
"..."
)}
{props.selectGroupTop && (
props.onChange?.(props.selectGroupTop!.name)}
>
)}
{hidden.map((select: SelectItemProps, idx: number) => (
))}
{props.selectGroupBottom && (
props.onChange?.(props.selectGroupBottom!.name)}
>
)}
)}
);
}
function SelectGroupMobile(props: SelectGroupProps) {
return (
{
props.onChange?.(value);
}}
>
{props.selectGroupTop && (
props.onChange?.(props.selectGroupTop!.name)}
>
)}
{props.list.map((select: SelectItemProps, idx: number) => (
))}
{props.selectGroupBottom && (
props.onChange?.(props.selectGroupBottom!.name)}
>
)}
);
}
function SelectGroup(props: SelectGroupProps) {
const [state, setState] = useState(mobile);
useEffect(() => {
window.addEventListener("resize", () => {
setState(mobile);
});
}, []);
return state ? (
) : (
);
}
export default SelectGroup;
================================================
FILE: app/src/components/ThemeProvider.tsx
================================================
import { createContext, useContext, useEffect, useState, ReactNode } from "react";
import { Moon, Sun, Monitor } from "lucide-react";
import { Button } from "./ui/button";
import { getMemory, setMemory } from "@/utils/memory.ts";
import { themeEvent } from "@/events/theme.ts";
const defaultTheme: Theme = "dark";
export type Theme = "dark" | "light" | "system";
type ThemeProviderProps = {
children?: ReactNode;
defaultTheme?: Theme;
};
type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleTheme?: () => void;
};
export function activeTheme(theme: Theme) {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
let actualTheme = theme;
if (theme === "system") {
actualTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
root.classList.add(actualTheme);
setMemory("theme", theme);
themeEvent.emit(actualTheme);
}
export function getTheme() {
return (getMemory("theme") as Theme) || defaultTheme;
}
// system -> dark -> light -> system
function getNextTheme(current: Theme): Theme {
return current === "system" ? "dark" : current === "dark" ? "light" : "system";
}
const initialState: ThemeProviderState = {
theme: "system",
setTheme: (theme: Theme) => {
activeTheme(theme);
},
toggleTheme: () => {
const key = getMemory("theme");
const current = (key.length > 0 ? (key as Theme) : defaultTheme) as Theme;
const next = getNextTheme(current);
activeTheme(next);
},
};
const ThemeProviderContext = createContext(initialState);
export function ThemeProvider({
defaultTheme = "dark",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState(
() => (getMemory("theme") as Theme) || defaultTheme,
);
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
root.classList.add(systemTheme);
return;
}
root.classList.add(theme);
}, [theme]);
const value = {
theme,
setTheme: (newTheme: Theme) => {
activeTheme(newTheme);
setTheme(newTheme);
},
toggleTheme: () => {
const nextTheme: Theme = getNextTheme(theme);
activeTheme(nextTheme);
setTheme(nextTheme);
},
};
return ;
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider");
return context;
};
export function ThemeToggle({ className, size = "icon" }: { className?: string; size?: "icon" | "icon-md" }) {
const { theme, toggleTheme } = useTheme();
return (
toggleTheme?.()}
className={`!m-0 ${className || ''}`}
>
);
}
export default ThemeToggle;
================================================
FILE: app/src/components/ThinkContent.tsx
================================================
import { useState } from "react";
import { ChevronDown, ChevronUp, Brain, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/components/ui/lib/utils";
import Markdown from "@/components/Markdown";
import { motion, AnimatePresence } from "framer-motion";
import { useTranslation } from "react-i18next";
interface ThinkContentProps {
content: string;
isComplete?: boolean;
}
export function ThinkContent({ content, isComplete = true }: ThinkContentProps) {
const [isExpanded, setIsExpanded] = useState(true);
const { t } = useTranslation();
const toggleExpand = () => {
setIsExpanded(!isExpanded);
};
return (
{t("message.thinking-process")}
{!isComplete && (
)}
{isExpanded ? (
) : (
)}
{isExpanded && (
{content}
)}
);
}
================================================
FILE: app/src/components/TickButton.tsx
================================================
import { Button, ButtonProps } from "@/components/ui/button.tsx";
import React, { useEffect, useRef, useState } from "react";
import { isAsyncFunc } from "@/utils/base.ts";
export interface TickButtonProps extends ButtonProps {
tick: number;
onTickChange?: (tick: number) => void;
onClick?: (
e: React.MouseEvent,
) => boolean | Promise;
}
function TickButton({
tick,
onTickChange,
onClick,
children,
...props
}: TickButtonProps) {
const stamp = useRef(0);
const [timer, setTimer] = useState(0);
useEffect(() => {
setInterval(() => {
const offset = Math.floor((Number(Date.now()) - stamp.current) / 1000);
let value = tick - offset;
if (value <= 0) value = 0;
setTimer(value);
onTickChange && onTickChange(value);
}, 250);
}, []);
const onReset = () => (stamp.current = Number(Date.now()));
// if is async function, use this:
const onTrigger = isAsyncFunc(onClick)
? async (e: React.MouseEvent) => {
if (timer !== 0 || !onClick) return;
if (await onClick(e)) onReset();
}
: (e: React.MouseEvent) => {
if (timer !== 0 || !onClick) return;
if (onClick(e)) onReset();
};
return (
{timer === 0 ? children : `${timer}s`}
);
}
export function useTicker(
interval?: number,
onTickerEnd?: () => any,
): {
tick: number;
triggerTicker: () => void;
} {
const stamp = useRef(0);
const [tick, setTick] = useState(0);
const step = interval || 60;
const triggerTicker = () => (stamp.current = Number(Date.now()));
useEffect(() => {
setInterval(() => {
const offset = Math.floor((Number(Date.now()) - stamp.current) / 1000);
setTick(step - offset);
}, 250);
}, []);
useEffect(() => {
if (stamp.current === 0) return;
if (tick === 0) {
onTickerEnd && onTickerEnd();
stamp.current = 0;
}
}, [tick]);
return {
tick: tick < 0 ? 0 : tick > step ? step : tick, // fix negative value
triggerTicker,
};
}
export default TickButton;
================================================
FILE: app/src/components/Tips.tsx
================================================
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip.tsx";
import { HelpCircle } from "lucide-react";
import React, { useEffect, useMemo, useRef } from "react";
import { cn } from "@/components/ui/lib/utils.ts";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu.tsx";
import Clickable from "@/components/ui/clickable.tsx";
type TipsProps = {
content?: string;
align?: "start" | "end" | "center" | undefined;
side?: "top" | "bottom" | "left" | "right" | undefined;
trigger?: React.ReactNode;
children?: React.ReactNode;
className?: string;
classNameTrigger?: string;
classNamePopup?: string;
hideTimeout?: number;
notHide?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
onClicked?: () => void;
asChild?: boolean;
};
function Tips({
content,
align,
side,
trigger,
children,
className,
classNameTrigger,
classNamePopup,
hideTimeout,
notHide,
open,
onOpenChange,
onClicked,
asChild,
}: TipsProps) {
const timeout = hideTimeout ?? 2500;
const comp = useMemo(
() => (
<>
{content && {content}
}
{children}
>
),
[content, children],
);
const [drop, setDrop] = onOpenChange
? [open, onOpenChange]
: React.useState(false);
const [tooltip, setTooltip] = React.useState(false);
const task = useRef();
useEffect(() => {
if (notHide) return;
drop
? (task.current = setTimeout(() => setDrop(false), timeout))
: clearTimeout(task.current);
}, [drop]);
useEffect(() => {
if (!tooltip) return;
setTooltip(false);
!drop && setDrop(true);
}, [drop, tooltip]);
return (
{
setDrop(open);
open && onClicked && onClicked();
}}
>
{trigger ?? (
)}
{comp}
);
}
export default Tips;
================================================
FILE: app/src/components/TrendBadge.tsx
================================================
import React from "react";
import { ChevronDown, ChevronUp } from "lucide-react";
export type TrendBadgeProps = {
current: number;
previous: number;
};
export const TrendBadge: React.FC = ({
current,
previous,
}) => {
const trend = previous === 0 ? 0 : ((current - previous) / previous) * 100;
const percentage = Math.abs(trend).toFixed(1);
return trend < 0 ? (
{percentage}%
) : (
{percentage}%
);
};
================================================
FILE: app/src/components/VoiceProvider.tsx
================================================
import { ChatAction } from "@/components/home/assemblies/ChatAction.tsx";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Mic } from "lucide-react";
export function VoiceAction() {
const { t } = useTranslation();
return (
toast.info(t("coming-soon"))}
>
);
}
================================================
FILE: app/src/components/WarningButton.tsx
================================================
import React, { useState } from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogEmoji,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button, type ButtonProps } from "@/components/ui/button";
import { cn } from "@/components/ui/lib/utils";
import { useTranslation } from "react-i18next";
type WarningButtonProps = ButtonProps & {
title?: string;
description?: string;
confirmText?: string;
cancelText?: string;
children?: React.ReactNode;
};
export default function WarningButton({
variant,
title,
description,
confirmText,
cancelText,
children,
className,
onClick,
...props
}: WarningButtonProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const handleClick = onClick
? async () => {
//@ts-ignore
await onClick?.();
setOpen(false);
}
: undefined;
return (
{children}
{title || t("are-you-sure")}
{description || t("this-action-cannot-be-undone")}
{cancelText || t("cancel")}
{confirmText || t("confirm")}
);
}
================================================
FILE: app/src/components/admin/ChannelSettings.tsx
================================================
import { useEffect, useReducer, useState } from "react";
import ChannelTable from "@/components/admin/assemblies/ChannelTable.tsx";
import ChannelEditor from "@/components/admin/assemblies/ChannelEditor.tsx";
import { Channel, getChannelInfo } from "@/admin/channel.ts";
import { useSearchParams } from "react-router-dom";
const initialProxyState = {
proxy: "",
proxy_type: 0,
username: "",
password: "",
};
const initialState: Channel = {
id: -1,
type: "openai",
name: "",
models: [],
priority: 0,
weight: 1,
retry: 3,
secret: "",
endpoint: getChannelInfo().endpoint,
mapper: "",
state: true,
group: [],
proxy: { ...initialProxyState },
};
function reducer(state: Channel, action: any): Channel {
switch (action.type) {
case "type":
const isChanged =
getChannelInfo(state.type).endpoint !== state.endpoint &&
state.endpoint.trim() !== "";
const endpoint = isChanged
? state.endpoint
: getChannelInfo(action.value).endpoint;
return { ...state, endpoint, type: action.value };
case "name":
return { ...state, name: action.value };
case "models":
return { ...state, models: action.value };
case "add-model":
if (state.models.includes(action.value) || action.value === "") {
return state;
}
return { ...state, models: [...state.models, action.value] };
case "add-models":
const models = action.value.filter(
(model: string) => !state.models.includes(model) && model !== "",
);
return { ...state, models: [...state.models, ...models] };
case "remove-model":
return {
...state,
models: state.models.filter((model) => model !== action.value),
};
case "clear-models":
return { ...state, models: [] };
case "priority":
return { ...state, priority: action.value };
case "weight":
return { ...state, weight: action.value };
case "secret":
return { ...state, secret: action.value };
case "endpoint":
return { ...state, endpoint: action.value };
case "mapper":
return { ...state, mapper: action.value };
case "retry":
return { ...state, retry: action.value };
case "clear":
return { ...initialState };
case "add-group":
return {
...state,
group: state.group ? [...state.group, action.value] : [action.value],
};
case "remove-group":
return {
...state,
group: state.group
? state.group.filter((group) => group !== action.value)
: [],
};
case "set-group":
return { ...state, group: action.value };
case "set-proxy":
return {
...state,
proxy: {
proxy: action.value as string,
proxy_type: state?.proxy?.proxy_type || 0,
password: state?.proxy?.password || "",
username: state?.proxy?.username || "",
},
};
case "set-proxy-type":
return {
...state,
proxy: {
proxy: state?.proxy?.proxy || "",
proxy_type: action.value as number,
password: state?.proxy?.password || "",
username: state?.proxy?.username || "",
},
};
case "set-proxy-username":
return {
...state,
proxy: {
proxy: state?.proxy?.proxy || "",
proxy_type: state?.proxy?.proxy_type || 0,
password: state?.proxy?.password || "",
username: action.value as string,
},
};
case "set-proxy-password":
return {
...state,
proxy: {
proxy: state?.proxy?.proxy || "",
proxy_type: state?.proxy?.proxy_type || 0,
password: action.value as string,
username: state?.proxy?.username || "",
},
};
case "set-first-message-as-user":
return { ...state, first_message_as_user: action.value };
case "set-merge-consecutive-user-messages":
return { ...state, merge_consecutive_user_messages: action.value };
case "set":
return { ...state, ...action.value };
case "import":
return { ...state, ...action.value, id: state.id, state: state.state };
default:
return state;
}
}
function ChannelSettings() {
const [search] = useSearchParams();
const [enabled, setEnabled] = useState(
search.get("editor_id") !== null && search.get("editor_id") !== "empty",
);
const [id, setId] = useState(
search.get("editor_id") !== null && search.get("editor_id") !== "empty"
? parseInt(search.get("editor_id") || "-1")
: -1,
);
const [data, setData] = useState([]);
const [edit, dispatch] = useReducer(reducer, { ...initialState });
useEffect(() => {
// set uri to ?editor_id=${id} if enabled is true, otherwise remove it
if (enabled) {
window.history.replaceState({}, "", `?editor_id=${id}`);
} else {
window.history.replaceState({}, "", "?editor_id=empty");
}
}, [enabled, id]);
return (
<>
>
);
}
export default ChannelSettings;
================================================
FILE: app/src/components/admin/ChargeWidget.tsx
================================================
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group.tsx";
import { Label } from "@/components/ui/label.tsx";
import {
ChargeProps,
chargeTypes,
defaultChargeType,
nonBilling,
timesBilling,
tokenBilling,
} from "@/admin/charge.ts";
import { useTranslation } from "react-i18next";
import { Input } from "@/components/ui/input.tsx";
import { useMemo, useReducer, useState } from "react";
import { Button } from "@/components/ui/button.tsx";
import {
Activity,
AlertCircle,
BoxIcon,
Cloud,
Copy,
DownloadCloud,
Eraser,
EyeOff,
KanbanSquareDashed,
Minus,
PencilLine,
Plus,
RotateCw,
Search,
Settings2,
Trash,
UploadCloud,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu.tsx";
import {
Command,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command.tsx";
import { withNotify } from "@/api/common.ts";
import { Switch } from "@/components/ui/switch.tsx";
import { NumberInput } from "@/components/ui/number-input.tsx";
import {
Table,
TableBody,
TableCell,
TableHeader,
TableRow,
} from "@/components/ui/table.tsx";
import OperationAction from "@/components/OperationAction.tsx";
import { Badge } from "@/components/ui/badge.tsx";
import {
deleteCharge,
listCharge,
setCharge,
syncCharge,
fetchUpstreamCharge,
} from "@/admin/api/charge.ts";
import { useEffectAsync } from "@/utils/hook.ts";
import { cn } from "@/components/ui/lib/utils.ts";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
import Tips from "@/components/Tips.tsx";
import { getQuerySelector, scrollUp, useClipboard } from "@/utils/dom.ts";
import PopupDialog, { popupTypes } from "@/components/PopupDialog.tsx";
import { getV1Path } from "@/api/v1.ts";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog.tsx";
import { getUniqueList, isEnter, parseNumber } from "@/utils/base.ts";
import { defaultChannelModels } from "@/admin/channel.ts";
import { getPricing } from "@/admin/datasets/charge.ts";
import { useAllModels } from "@/admin/hook.tsx";
import { toast } from "sonner";
import { formatDecimal } from "@/utils/base.ts";
const initialState: ChargeProps = {
id: -1,
type: defaultChargeType,
models: [],
anonymous: false,
input: 0,
output: 0,
};
function reducer(state: ChargeProps, action: any): ChargeProps {
switch (action.type) {
case "set":
return { ...action.payload };
case "set-models":
return { ...state, models: action.payload };
case "add-model":
const model = action.payload.trim();
if (model.length === 0 || state.models.includes(model)) return state;
return { ...state, models: [...state.models, model] };
case "toggle-model":
if (action.payload.trim().length === 0) return state;
return state.models.includes(action.payload)
? {
...state,
models: state.models.filter((model) => model !== action.payload),
}
: { ...state, models: [...state.models, action.payload] };
case "remove-model":
return {
...state,
models: state.models.filter((model) => model !== action.payload),
};
case "set-type":
return { ...state, type: action.payload };
case "set-anonymous":
return { ...state, anonymous: action.payload };
case "set-input":
return { ...state, input: action.payload };
case "set-output":
return { ...state, output: action.payload };
case "clear":
return initialState;
case "clear-param":
return { ...initialState, id: state.id };
default:
return state;
}
}
function preflight(state: ChargeProps): ChargeProps {
state.models = state.models
.map((model) => model.trim())
.filter((model) => model.length > 0);
switch (state.type) {
case nonBilling:
state.input = 0;
state.output = 0;
break;
case timesBilling:
state.input = 0;
state.anonymous = false;
break;
case tokenBilling:
state.anonymous = false;
break;
}
if (state.input < 0) state.input = 0;
if (state.output < 0) state.output = 0;
return state;
}
type SyncDialogProps = {
current: string[];
builtin: boolean;
open: boolean;
setOpen: (open: boolean) => void;
onRefresh: () => void;
system: string;
};
function SyncDialog({
builtin,
current,
open,
setOpen,
onRefresh,
system,
}: SyncDialogProps) {
const { t } = useTranslation();
const [siteCharge, setSiteCharge] = useState([]);
const [siteOpen, setSiteOpen] = useState(false);
const [overwrite, setOverwrite] = useState(false);
const siteModels = useMemo(
() => siteCharge.flatMap((charge) => charge.models),
[siteCharge],
);
const influencedModels = useMemo(
() =>
overwrite
? siteModels
: siteModels.filter((model) => !current.includes(model)),
[overwrite, siteModels, current],
);
return (
<>
=> {
const currency = parseNumber(_currency);
const pricing = getPricing(currency);
setSiteCharge(pricing);
setSiteOpen(true);
return true;
}}
/>
=> {
const path = system === "newapi"
? `${endpoint.replace(/\/$/, "")}/api/ratio_config`
: getV1Path("/v1/charge", { endpoint });
const resp = await fetchUpstreamCharge({ endpoint, system });
if (!resp.status || resp.data.length === 0) {
toast.error(t("admin.charge.sync-failed"), {
description: t("admin.charge.sync-failed-prompt", {
endpoint: path,
}),
});
return false;
}
setSiteCharge(resp.data);
setSiteOpen(true);
return true;
}}
/>
{t("admin.charge.sync-option")}
{t("admin.charge.sync-prompt", {
length: siteModels.length,
influence: influencedModels.length,
})}
{t("admin.charge.sync-overwrite")}
{
setSiteOpen(false);
setSiteCharge([]);
}}
>
{t("cancel")}
{
const resp = await syncCharge({
data: siteCharge,
overwrite,
});
withNotify(t, resp, true);
if (resp.status) {
setOpen(false);
setSiteOpen(false);
setSiteCharge([]);
onRefresh();
}
}}
>
{t("admin.charge.sync-confirm")}
>
);
}
type ChargeActionProps = {
loading: boolean;
onRefresh: () => void;
currentModels: string[];
};
function ChargeAction({
loading,
onRefresh,
currentModels,
}: ChargeActionProps) {
const { t } = useTranslation();
const [popup, setPopup] = useState(false);
const [builtin, setBuiltin] = useState(false);
const [system, setSystem] = useState("");
const open = (builtin: boolean) => {
setBuiltin(builtin);
setPopup(true);
};
return (
open(true)}>
{t("admin.charge.sync-builtin")}
{t("admin.charge.sync")}
{
setSystem("");
open(false);
}}
>
CoAI
{
setSystem("newapi");
open(false);
}}
>
NewAPI
);
}
type ChargeAlertProps = {
models: string[];
onClick: (model: string) => void;
};
function ChargeAlert({ models, onClick }: ChargeAlertProps) {
const { t } = useTranslation();
return (
models.length > 0 && (
{t("admin.charge.unused-model")}
{models.slice(0, 15).map((model, index) => (
onClick(model)}
>
{model}
))}
)
);
}
type ChargeEditorProps = {
form: ChargeProps;
dispatch: (action: any) => void;
onRefresh: () => void;
usedModels: string[];
allModels: string[];
};
function ChargeEditor({
form,
dispatch,
onRefresh,
usedModels,
allModels,
}: ChargeEditorProps) {
const { t } = useTranslation();
const [model, setModel] = useState("");
const channelModels = useMemo(
() => getUniqueList([...allModels, ...defaultChannelModels]),
[allModels],
);
const unusedModels = useMemo(() => {
return channelModels.filter(
(model) =>
!form.models.includes(model) &&
!usedModels.includes(model) &&
model.trim() !== "",
);
}, [form.models, usedModels]);
const disabled = useMemo(() => {
if (model.trim() !== "") return false;
return form.models.length === 0;
}, [model, form.models]);
const [loading, setLoading] = useState(false);
async function post() {
const raw = model.trim();
const data = preflight({ ...form });
if (raw !== "" && !data.models.includes(raw)) {
data.models = [raw, ...data.models];
setModel("");
}
const resp = await setCharge(data);
withNotify(t, resp, true);
if (resp.status) clear();
onRefresh();
}
function clear() {
dispatch({ type: "clear" });
setModel("");
}
return (
dispatch({ type: "set-type", payload: value })
}
className={`flex flex-row gap-5 whitespace-nowrap flex-wrap`}
>
{chargeTypes.map((chargeType, index) => (
{t(`admin.charge.${chargeType}`)}
))}
{form.models.map((model, index) => (
dispatch({ type: "remove-model", payload: model })}
size={`icon`}
variant={`outline`}
className={`ml-2 shrink-0`}
>
))}
{form.type === nonBilling && (
{t("admin.charge.anonymous")}
dispatch({ type: "set-anonymous", payload: checked })
}
/>
)}
{form.type === timesBilling && (
{t("admin.charge.time-count")}
dispatch({ type: "set-output", payload: value })
}
acceptNegative={false}
className={`w-20`}
min={0}
max={99999}
/>
)}
{form.type === tokenBilling && (
{t("admin.charge.input-count")}
/ 1k tokens
dispatch({ type: "set-input", payload: value })
}
acceptNegative={false}
className={`w-20`}
min={0}
max={99999}
/>
{t("admin.charge.output-count")}
/ 1k tokens
dispatch({ type: "set-output", payload: value })
}
acceptNegative={false}
className={`w-20`}
min={0}
max={99999}
/>
)}
ID
{form.id === -1 ? (
) : (
{form.id}
)}
{form.id === -1 ? (
<>
{!loading && }
{t("admin.charge.add-rule")}
>
) : (
<>
{!loading && }
{t("admin.charge.update-rule")}
>
)}
);
}
type ChargeTableProps = {
data: ChargeProps[];
dispatch: (action: any) => void;
onRefresh: () => void;
};
function ChargeTable({ data, dispatch, onRefresh }: ChargeTableProps) {
const { t } = useTranslation();
const copy = useClipboard();
return (
{t("admin.charge.id")}
{t("admin.charge.type")}
{t("admin.charge.model")}
{t("admin.charge.input")}
{t("admin.charge.output")}
{t("admin.charge.support-anonymous")}
{t("admin.charge.action")}
{data.map((charge, idx) => (
{charge.id}
{t(`admin.charge.${charge.type}`)}
{charge.models.map((model, index) => (
copy(model)}
>
{model}
))}
{formatDecimal(charge.input)}
{formatDecimal(charge.output)}
{t(String(charge.anonymous))}
{
const props: ChargeProps = { ...charge };
dispatch({ type: "set", payload: props });
// scroll to top
scrollUp(
getQuerySelector(
".admin-content > .scrollarea-viewport",
)!,
);
}}
>
{
const resp = await deleteCharge(charge.id);
withNotify(t, resp, true);
onRefresh();
}}
>
))}
);
}
function ChargeWidget() {
const { t } = useTranslation();
const [data, setData] = useState([]);
const [form, dispatch] = useReducer(reducer, initialState);
const [loading, setLoading] = useState(false);
const { allModels, update } = useAllModels();
const currentModels = useMemo(() => {
return data.flatMap((charge) => charge.models);
}, [data]);
const usedModels = useMemo((): string[] => {
return data.flatMap((charge) => charge.models);
}, [data]);
const unusedModels = useMemo(() => {
if (loading) return [];
return allModels.filter(
(model) => !usedModels.includes(model) && model.trim() !== "",
);
}, [loading, allModels, usedModels]);
async function refresh(ignoreUpdate?: boolean) {
setLoading(true);
const resp = await listCharge();
if (!ignoreUpdate) await update();
setLoading(false);
withNotify(t, resp);
setData(resp.data);
}
useEffectAsync(async () => await refresh(true), []);
return (
dispatch({ type: "toggle-model", payload: model })}
/>
);
}
export default ChargeWidget;
================================================
FILE: app/src/components/admin/ChartBox.tsx
================================================
import ModelChart from "@/components/admin/assemblies/ModelChart.tsx";
import { useState } from "react";
import {
BillingChartResponse,
ErrorChartResponse,
ModelChartResponse,
RequestChartResponse,
UserTypeChartResponse,
} from "@/admin/types.ts";
import RequestChart from "@/components/admin/assemblies/RequestChart.tsx";
import BillingChart from "@/components/admin/assemblies/BillingChart.tsx";
import ErrorChart from "@/components/admin/assemblies/ErrorChart.tsx";
import { useEffectAsync } from "@/utils/hook.ts";
import {
getBillingChart,
getErrorChart,
getModelChart,
getRequestChart,
getUserTypeChart,
} from "@/admin/api/chart.ts";
import ModelUsageChart from "@/components/admin/assemblies/ModelUsageChart.tsx";
import UserTypeChart from "@/components/admin/assemblies/UserTypeChart.tsx";
function ChartBox() {
const [model, setModel] = useState({
date: [],
value: [],
});
const [request, setRequest] = useState({
date: [],
value: [],
});
const [billing, setBilling] = useState({
date: [],
value: [],
});
const [error, setError] = useState({
date: [],
value: [],
});
const [user, setUser] = useState({
total: 0,
normal: 0,
api_paid: 0,
basic_plan: 0,
standard_plan: 0,
pro_plan: 0,
});
useEffectAsync(async () => {
setModel(await getModelChart());
setRequest(await getRequestChart());
setBilling(await getBillingChart());
setError(await getErrorChart());
setUser(await getUserTypeChart());
}, []);
return (
);
}
export default ChartBox;
================================================
FILE: app/src/components/admin/InfoBox.tsx
================================================
import { useTranslation } from "react-i18next";
import { useState } from "react";
import {
CircleDollarSign,
MessageSquareDot,
Users2,
Wallet,
} from "lucide-react";
import { useEffectAsync } from "@/utils/hook.ts";
import { getAdminInfo, initialAdminInfoState } from "@/admin/api/chart.ts";
import { InfoResponse } from "@/admin/types.ts";
import { getReadableNumber } from "@/utils/processor.ts";
import { TrendBadge } from "@/components/TrendBadge.tsx";
import { useCurrency } from "@/store/info";
function InfoBox() {
const { t } = useTranslation();
const { name: currencyName } = useCurrency();
const [form, setForm] = useState({
...initialAdminInfoState,
});
useEffectAsync(async () => {
setForm(await getAdminInfo());
}, []);
return (
{t("admin.billing-today")}
{form.billing_today.toFixed(2)}
{currencyName}
{t("admin.billing-month")}
{form.billing_month.toFixed(2)}
{currencyName}
{t("admin.subscription-users")}
{form.subscription_count}
{t("admin.seat")}
{t("admin.online-chats")}
{getReadableNumber(form.online_chats)}
);
}
export default InfoBox;
================================================
FILE: app/src/components/admin/InvitationTable.tsx
================================================
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table.tsx";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog.tsx";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import { InvitationForm, InvitationResponse } from "@/admin/types.ts";
import { Button, TemporaryButton } from "@/components/ui/button.tsx";
import { Copy, Download, Loader2, RotateCw, Trash } from "lucide-react";
import { useEffectAsync } from "@/utils/hook.ts";
import {
deleteInvitation,
generateInvitation,
getInvitationList,
} from "@/admin/api/chart.ts";
import { Input } from "@/components/ui/input.tsx";
import { Textarea } from "@/components/ui/textarea.tsx";
import { copyClipboard, saveAsFile } from "@/utils/dom.ts";
import { PaginationAction } from "@/components/ui/pagination.tsx";
import { Badge } from "@/components/ui/badge.tsx";
import OperationAction from "@/components/OperationAction.tsx";
import { withNotify } from "@/api/common.ts";
import StateBadge from "@/components/admin/common/StateBadge.tsx";
import { toast } from "sonner";
function GenerateDialog({ update }: { update: () => void }) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [type, setType] = useState("");
const [quota, setQuota] = useState("5");
const [number, setNumber] = useState("1");
const [data, setData] = useState("");
function getNumber(value: string): string {
return value.replace(/[^\d.]/g, "");
}
async function generateCode() {
const data = await generateInvitation(type, Number(quota), Number(number));
if (data.status) {
setData(data.data.join("\n"));
update();
} else
toast.error(t("admin.error"), {
description: data.message,
});
}
function close() {
setType("");
setQuota("5");
setNumber("1");
setOpen(false);
setData("");
}
function downloadCode() {
return saveAsFile("invitation.txt", data);
}
return (
<>
{t("admin.generate")}
{t("admin.generate")}
setOpen(false)}>
{t("admin.cancel")}
{t("admin.confirm")}
{
if (!state) close();
}}
>
{t("admin.generate-result")}
{t("close")}
{t("download")}
>
);
}
function InvitationTable() {
const { t } = useTranslation();
const [data, setData] = useState({
total: 0,
data: [],
});
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(0);
async function update() {
setLoading(true);
const resp = await getInvitationList(page);
setLoading(false);
if (resp.status) setData(resp as InvitationResponse);
else
toast.error(t("admin.error"), {
description: resp.message,
});
}
useEffectAsync(update, [page]);
return (
{(data.data && data.data.length > 0) || page > 0 ? (
<>
{t("admin.invitation-code")}
{t("admin.quota")}
{t("admin.type")}
{t("admin.used")}
{t("admin.used-username")}
{t("admin.created-at")}
{t("admin.used-at")}
{t("admin.action")}
{(data.data || []).map((invitation, idx) => (
{invitation.code}
{invitation.quota}
{invitation.type}
{invitation.username || "-"}
{invitation.created_at}
{invitation.updated_at}
copyClipboard(invitation.code)}
>
{
const resp = await deleteInvitation(invitation.code);
withNotify(t, resp, true);
resp.status && (await update());
}}
>
))}
>
) : (
{loading ? (
) : (
{t("admin.empty")}
)}
)}
);
}
export default InvitationTable;
================================================
FILE: app/src/components/admin/MenuBar.tsx
================================================
import { useDispatch, useSelector } from "react-redux";
import { closeMenu, selectMenu } from "@/store/menu.ts";
import React, { useMemo } from "react";
import {
BookCopy,
CalendarRange,
CloudCog,
CopyrightIcon,
CreditCard,
FileClock,
Gauge,
GitFork,
History,
Radio,
ServerCrash,
Settings,
Users,
} from "lucide-react";
import router from "@/router.tsx";
import { useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { mobile } from "@/utils/device.ts";
import { cn } from "@/components/ui/lib/utils.ts";
import { Badge } from "@/components/ui/badge.tsx";
type MenuItemProps = {
title: string;
icon: React.ReactNode;
path: string;
exit?: boolean;
pro?: boolean;
};
function MenuItem({ title, icon, path, exit, pro }: MenuItemProps) {
const location = useLocation();
const dispatch = useDispatch();
const active = useMemo(
() =>
!exit &&
(location.pathname === `/admin${path}` ||
location.pathname + "/" === `/admin${path}`),
[location.pathname, path],
);
const redirect = async () => {
if (exit) return await router.navigate("/");
if (mobile) dispatch(closeMenu());
await router.navigate(`/admin${path}`);
};
return (
{icon}
{title}
{pro && (
Pro
)}
);
}
function MenuBar() {
const { t } = useTranslation();
const open = useSelector(selectMenu);
return (
} path={"/"} />
} path={"/users"} />
}
path={"/market"}
/>
}
path={"/broadcast"}
/>
}
path={"/channel"}
/>
} path={"/charge"} />
}
path={"/subscription"}
/>
}
path={"/pay"}
pro
/>
}
path={"/record"}
/>
}
path={"/system"}
/>
}
path={"/logger"}
/>
}
path={"/warmup"}
/>
}
path={"/license"}
/>
);
}
export default MenuBar;
================================================
FILE: app/src/components/admin/RedeemTable.tsx
================================================
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table.tsx";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog.tsx";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import { RedeemForm, RedeemResponse } from "@/admin/types.ts";
import { Button, TemporaryButton } from "@/components/ui/button.tsx";
import { Copy, Download, Loader2, RotateCw, Trash } from "lucide-react";
import {
deleteRedeem,
generateRedeem,
getRedeemList,
} from "@/admin/api/chart.ts";
import { Input } from "@/components/ui/input.tsx";
import { Textarea } from "@/components/ui/textarea.tsx";
import { copyClipboard, saveAsFile } from "@/utils/dom.ts";
import { useEffectAsync } from "@/utils/hook.ts";
import { Badge } from "@/components/ui/badge.tsx";
import { PaginationAction } from "@/components/ui/pagination.tsx";
import OperationAction from "@/components/OperationAction.tsx";
import { withNotify } from "@/api/common.ts";
import StateBadge from "@/components/admin/common/StateBadge.tsx";
import { toast } from "sonner";
function GenerateDialog({ update }: { update: () => void }) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [quota, setQuota] = useState("5");
const [number, setNumber] = useState("1");
const [data, setData] = useState("");
function getNumber(value: string): string {
return value.replace(/[^\d.]/g, "");
}
async function generateCode() {
const data = await generateRedeem(Number(quota), Number(number));
if (data.status) {
setData(data.data.join("\n"));
update();
} else {
toast.error(t("admin.error"), {
description: data.message,
});
}
}
function close() {
setQuota("5");
setNumber("1");
setOpen(false);
setData("");
}
function downloadCode() {
return saveAsFile("code.txt", data);
}
return (
<>
{t("admin.generate")}
{t("admin.generate")}
setOpen(false)}
>
{t("admin.cancel")}
{t("admin.confirm")}
{
if (!state) close();
}}
>
{t("admin.generate-result")}
{t("close")}
{t("download")}
>
);
}
function RedeemTable() {
const { t } = useTranslation();
const [data, setData] = useState({
total: 0,
data: [],
});
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(0);
async function update() {
setLoading(true);
const resp = await getRedeemList(page);
setLoading(false);
if (resp.status) setData(resp as RedeemResponse);
else
toast.error(t("admin.error"), {
description: resp.message,
});
}
useEffectAsync(update, [page]);
return (
{(data.data && data.data.length > 0) || page > 0 ? (
<>
{t("admin.redeem.code")}
{t("admin.redeem.quota")}
{t("admin.used")}
{t("admin.created-at")}
{t("admin.used-at")}
{t("admin.action")}
{(data.data || []).map((redeem, idx) => (
{redeem.code}
{redeem.quota}
{redeem.created_at}
{redeem.updated_at}
copyClipboard(redeem.code)}
>
{
const resp = await deleteRedeem(redeem.code);
withNotify(t, resp, true);
resp.status && (await update());
}}
>
))}
>
) : loading ? (
) : (
{t("admin.empty")}
)}
);
}
export default RedeemTable;
================================================
FILE: app/src/components/admin/UserTable.tsx
================================================
import { useTranslation } from "react-i18next";
import { useMemo, useReducer, useState } from "react";
import {
CommonResponse,
UserData,
UserForm,
UserResponse,
} from "@/admin/types.ts";
import {
banUserOperation,
getUserList,
initialUserFilter,
quotaOperation,
releaseUsageOperation,
setAdminOperation,
subscriptionLevelOperation,
subscriptionOperation,
updateEmail,
updatePassword,
UserFilterProps,
} from "@/admin/api/chart.ts";
import { useEffectAsync } from "@/utils/hook.ts";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table.tsx";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu.tsx";
import { Button } from "@/components/ui/button.tsx";
import {
ArrowDownNarrowWide,
CalendarCheck2,
CalendarClock,
CalendarOff,
CalendarPlus,
CloudCog,
CloudFog,
Filter,
KeyRound,
Loader2,
Mail,
MinusCircle,
MoreHorizontal,
PlusCircle,
RotateCw,
Search,
Shield,
ShieldMinus,
} from "lucide-react";
import { Input } from "@/components/ui/input.tsx";
import PopupDialog, { popupTypes } from "@/components/PopupDialog.tsx";
import { getNumber, isEnter, parseNumber } from "@/utils/base.ts";
import { useSelector } from "react-redux";
import { selectUsername } from "@/store/auth.ts";
import { PaginationAction } from "@/components/ui/pagination.tsx";
import Tips from "@/components/Tips.tsx";
import {
Dialog,
DialogContent,
DialogDescription,
DialogTrigger,
DialogTitle,
DialogFooter,
DialogAction,
} from "@/components/ui/dialog.tsx";
import { RadioBox } from "@/components/ui/radio-box.tsx";
import { formReducer } from "@/utils/form.ts";
import { Separator } from "@/components/ui/separator.tsx";
import { toast } from "sonner";
import { Badge } from "../ui/badge";
type OperationMenuProps = {
user: UserData;
onRefresh?: () => void;
};
export enum UserType {
normal = "normal",
basic_plan = "basic_plan",
standard_plan = "standard_plan",
pro_plan = "pro_plan",
}
export const userTypeArray = [
UserType.normal,
UserType.basic_plan,
UserType.standard_plan,
UserType.pro_plan,
];
function doToast(t: any, resp: CommonResponse) {
if (!resp.status)
toast.error(t("admin.operate-failed"), {
description: t("admin.operate-failed-prompt", {
reason: resp.message || resp.error,
}),
});
else
toast.success(t("admin.operate-success"), {
description: t("admin.operate-success-prompt"),
});
}
function OperationMenu({ user, onRefresh }: OperationMenuProps) {
const { t } = useTranslation();
const username = useSelector(selectUsername);
const [passwordOpen, setPasswordOpen] = useState(false);
const [emailOpen, setEmailOpen] = useState(false);
const [quotaOpen, setQuotaOpen] = useState(false);
const [quotaSetOpen, setQuotaSetOpen] = useState(false);
const [subscriptionOpen, setSubscriptionOpen] = useState(false);
const [subscriptionLevelOpen, setSubscriptionLevelOpen] =
useState(false);
const [releaseOpen, setReleaseOpen] = useState(false);
const [banOpen, setBanOpen] = useState(false);
const [adminOpen, setAdminOpen] = useState(false);
return (
<>
{
const resp = await updatePassword(user.id, password);
doToast(t, resp);
if (resp.status) {
username === user.username && location.reload();
onRefresh?.();
}
return resp.status;
}}
/>
{
const resp = await updateEmail(user.id, email);
doToast(t, resp);
if (resp.status) onRefresh?.();
return resp.status;
}}
/>
{
const quota = parseNumber(value);
const resp = await quotaOperation(user.id, quota);
doToast(t, resp);
if (resp.status) onRefresh?.();
return resp.status;
}}
/>
{
const quota = parseNumber(value);
const resp = await quotaOperation(user.id, quota, true);
doToast(t, resp);
if (resp.status) onRefresh?.();
return resp.status;
}}
/>
{
const resp = await subscriptionOperation(user.id, value);
doToast(t, resp);
if (resp.status) onRefresh?.();
return resp.status;
}}
/>
{
const level = userTypeArray.indexOf(value as UserType);
console.log(level);
const resp = await subscriptionLevelOperation(user.id, level);
doToast(t, resp);
if (resp.status) onRefresh?.();
return resp.status;
}}
/>
{
const resp = await releaseUsageOperation(user.id);
doToast(t, resp);
if (resp.status) onRefresh?.();
return resp.status;
}}
/>
{
const resp = await banUserOperation(user.id, !user.is_banned);
doToast(t, resp);
if (resp.status) onRefresh?.();
return resp.status;
}}
/>
{
const resp = await setAdminOperation(user.id, !user.is_admin);
doToast(t, resp);
if (resp.status) onRefresh?.();
return resp.status;
}}
/>
setPasswordOpen(true)}>
{t("admin.password-action")}
setEmailOpen(true)}>
{t("admin.email-action")}
{user.is_banned ? (
setBanOpen(true)}>
{t("admin.unban-action")}
) : (
setBanOpen(true)}>
{t("admin.ban-action")}
)}
{user.is_admin ? (
setAdminOpen(true)}>
{t("admin.cancel-admin-action")}
) : (
setAdminOpen(true)}>
{t("admin.set-admin-action")}
)}
setQuotaOpen(true)}>
{t("admin.quota-action")}
setQuotaSetOpen(true)}>
{t("admin.quota-set-action")}
setSubscriptionOpen(true)}>
{t("admin.subscription-action")}
setSubscriptionLevelOpen(true)}>
{t("admin.subscription-level")}
setReleaseOpen(true)}>
{t("admin.release-subscription-action")}
>
);
}
function UserTable() {
const { t } = useTranslation();
const [data, setData] = useState({
total: 0,
data: [],
});
const [page, setPage] = useState(0);
const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false);
const [filter, filterDispatch] = useReducer(formReducer(), {
...initialUserFilter,
});
const [filterDialog, setFilterDialog] = useState(false);
const filterConds = useMemo((): number => {
return Object.values(filter).filter(
(value) => value !== "all" && value !== "id-asc",
).length;
}, [filter]);
async function update() {
setLoading(true);
const resp = await getUserList(page, search, filter);
setLoading(false);
if (resp.status) setData(resp as UserResponse);
else
toast.error(t("admin.error"), {
description: resp.message,
});
}
useEffectAsync(update, [page]);
return (
{filterConds > 0 && (
{filterConds}
)}
{t("filter.filter")}
{t("filter.conds", { count: filterConds })}
}
prefix="admin"
items={[
{ id: "all", value: t("filter.all") },
{ id: "yes", value: t("filter.admin") },
{ id: "no", value: t("filter.not-admin") },
]}
value={filter.admin}
onValueChange={(value) =>
filterDispatch({ type: "update:admin", value })
}
/>
}
prefix="ban"
items={[
{ id: "all", value: t("filter.all") },
{ id: "yes", value: t("filter.banned") },
{ id: "no", value: t("filter.not-banned") },
]}
value={filter.ban}
onValueChange={(value) =>
filterDispatch({ type: "update:ban", value })
}
/>
}
prefix="plan"
items={[
{ id: "all", value: t("filter.all") },
{ id: "yes", value: t("filter.subscribed") },
{ id: "no", value: t("filter.unsubscribed") },
]}
value={filter.plan}
onValueChange={(value) =>
filterDispatch({ type: "update:plan", value })
}
/>
}
prefix="sort"
colLayout
className={`mt-2`}
items={[
// id-desc, id-asc
{ id: "id-asc", value: t("filter.sorts.id-asc") },
{ id: "id-desc", value: t("filter.sorts.id-desc") },
// quota-desc, quota-asc
{ id: "quota-asc", value: t("filter.sorts.quota-asc") },
{ id: "quota-desc", value: t("filter.sorts.quota-desc") },
// used-quota-desc, used-quota-asc
{
id: "used-quota-asc",
value: t("filter.sorts.used-quota-asc"),
},
{
id: "used-quota-desc",
value: t("filter.sorts.used-quota-desc"),
},
// plan-desc, plan-asc
{ id: "plan-asc", value: t("filter.sorts.plan-asc") },
{ id: "plan-desc", value: t("filter.sorts.plan-desc") },
]}
value={filter.sort}
onValueChange={(value) =>
filterDispatch({ type: "update:sort", value })
}
/>
setFilterDialog(false)}>
{t("confirm")}
setSearch(e.target.value)}
onKeyDown={async (e) => {
if (isEnter(e)) await update();
}}
/>
{(data.data && data.data.length > 0) || page > 0 ? (
<>
ID
{t("admin.username")}
{t("admin.email")}
{t("admin.quota")}
{t("admin.used-quota")}
{t("admin.is-subscribed")}
{t("admin.level")}
{t("admin.total-month")}
{t("admin.expired-at")}
{t("admin.is-banned")}
{t("admin.is-admin")}
{t("admin.action")}
{(data.data || []).map((user, idx) => (
{user.id}
{user.username}
{user.email || "-"}
{user.quota}
{user.used_quota}
{t(user.is_subscribed.toString())}
{t(`admin.identity.${userTypeArray[user.level]}`)}
{user.total_month}
{user.expired_at || "-"}
{t(user.is_banned.toString())}
{t(user.is_admin.toString())}
))}
>
) : loading ? (
) : (
)}
);
}
export default UserTable;
================================================
FILE: app/src/components/admin/assemblies/BillingChart.tsx
================================================
import { useTranslation } from "react-i18next";
import { useMemo } from "react";
import { Loader2 } from "lucide-react";
import { AreaChart } from "@tremor/react";
import { useCurrency } from "@/store/info";
type BillingChartProps = {
labels: string[];
datasets: number[];
};
function BillingChart({ labels, datasets }: BillingChartProps) {
const { t } = useTranslation();
const { symbol } = useCurrency();
const data = useMemo(() => {
return datasets.map((data, index) => ({
date: labels[index],
[t("admin.billing")]: data,
}));
}, [labels, datasets, t("admin.billing")]);
const mrr = useMemo(() => {
// datasets sum
return datasets.reduce((acc, curr) => acc + curr, 0);
}, [datasets]);
return (
{t("admin.billing-chart")}
{labels.length === 0 && (
)}
MRR {symbol}
{mrr.toFixed(2)}
`${symbol}${value.toFixed(2)}`}
/>
);
}
export default BillingChart;
================================================
FILE: app/src/components/admin/assemblies/BroadcastTable.tsx
================================================
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table.tsx";
import { useState } from "react";
import { useSelector } from "react-redux";
import { selectInit } from "@/store/auth.ts";
import { useEffectAsync } from "@/utils/hook.ts";
import {
BroadcastInfo,
createBroadcast,
getBroadcastList,
removeBroadcast,
updateBroadcast,
} from "@/api/broadcast.ts";
import { useTranslation } from "react-i18next";
import { extractMessage } from "@/utils/processor.ts";
import { Button } from "@/components/ui/button.tsx";
import {
AlertCircle,
Edit,
Eye,
Loader2,
MoreVertical,
Plus,
RotateCcw,
Trash,
} from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog.tsx";
import { Textarea } from "@/components/ui/textarea.tsx";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu.tsx";
import EditorProvider from "@/components/EditorProvider.tsx";
import { Alert, AlertDescription } from "@/components/ui/alert.tsx";
import { withNotify } from "@/api/common.ts";
import {
AlertDialog,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog.tsx";
import { DialogClose } from "@radix-ui/react-dialog";
import { toast } from "sonner";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
type CreateBroadcastDialogProps = {
onCreated?: () => void;
};
function CreateBroadcastDialog(props: CreateBroadcastDialogProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [content, setContent] = useState("");
const [notifyAll, setNotifyAll] = useState(false);
async function postBroadcast() {
const broadcast = content.trim();
if (broadcast.length === 0) return;
const resp = await createBroadcast(broadcast, notifyAll);
if (resp.status) {
toast.success(t("admin.post-success"), {
description: t("admin.post-success-prompt"),
});
setContent("");
setNotifyAll(false);
setOpen(false);
props.onCreated?.();
} else {
toast.error(t("admin.post-failed"), {
description: t("admin.post-failed-prompt", { reason: resp.error }),
});
}
}
return (
{t("admin.create-broadcast")}
{t("admin.create-broadcast")}
{t("admin.broadcast-tip")}
{t("admin.cancel")}
{t("admin.post")}
);
}
type BroadcastItemProps = {
item: BroadcastInfo;
onRefresh?: () => void;
};
function BroadcastItem({ item, onRefresh }: BroadcastItemProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [dialogOpen, setDialogOpen] = useState(false);
const [value, setValue] = useState("");
return (
{
const resp = await updateBroadcast(item.index, value);
withNotify(t, resp, true);
onRefresh?.();
}}
/>
{t("admin.delete-broadcast")}
{t("admin.delete-broadcast-desc")}
{t("cancel")}
{
const resp = await removeBroadcast(item.index);
withNotify(t, resp, true);
onRefresh?.();
if (resp.status) setDialogOpen(false);
}}
>
{t("delete")}
{item.index}
{extractMessage(item.content, 25)}
{item.poster}
{item.created_at}
setOpen(true)}>
{t("admin.view")}
setOpen(true)}>
{t("edit")}
setDialogOpen(true)}>
{t("delete")}
);
}
function BroadcastTable() {
const { t } = useTranslation();
const init = useSelector(selectInit);
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const doRefresh = async () => {
if (!init) return;
setLoading(true);
setData(await getBroadcastList());
setLoading(false);
};
useEffectAsync(doRefresh, [init]);
return (
{
setData(await getBroadcastList());
}}
>
{t("admin.broadcast-tip")}
{data.length ? (
ID
{t("admin.broadcast-content")}
{t("admin.poster")}
{t("admin.post-at")}
{t("admin.action")}
{data.map((item, idx) => (
))}
) : (
{loading ? (
) : (
{t("admin.empty")}
)}
)}
);
}
export default BroadcastTable;
================================================
FILE: app/src/components/admin/assemblies/ChannelEditor.tsx
================================================
import Tips from "@/components/Tips.tsx";
import { Input } from "@/components/ui/input.tsx";
import {
Select,
SelectContent,
SelectItem,
SelectGroup,
SelectTrigger,
SelectValue,
NativeSelectTrigger,
} from "@/components/ui/select.tsx";
import {
Channel,
channelGroups,
ChannelTypes,
getChannelInfo,
proxyType,
ProxyTypes,
} from "@/admin/channel.ts";
import { CommonResponse, withNotify } from "@/api/common.ts";
import { FlexibleTextarea, Textarea } from "@/components/ui/textarea.tsx";
import { NumberInput } from "@/components/ui/number-input.tsx";
import { Button } from "@/components/ui/button.tsx";
import { useTranslation } from "react-i18next";
import { useMemo, useState } from "react";
import Required from "@/components/Require.tsx";
import {
BookDashed,
Loader2,
Paintbrush,
Plus,
Search,
Kanban,
X,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu.tsx";
import {
Command,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import Markdown from "@/components/Markdown.tsx";
import {
createChannel,
getChannel,
updateChannel,
} from "@/admin/api/channel.ts";
import { useEffectAsync } from "@/utils/hook.ts";
import Paragraph, {
ParagraphDescription,
ParagraphItem,
ParagraphSpace,
} from "@/components/Paragraph.tsx";
import { MultiCombobox } from "@/components/ui/multi-combobox.tsx";
import { useChannelModels } from "@/admin/hook.tsx";
import { isEnter } from "@/utils/base.ts";
import { TypeBadge } from "@/components/admin/assemblies/ChannelTable.tsx";
import { useClipboard } from "@/utils/dom.ts";
import { Switch } from "@/components/ui/switch.tsx";
type CustomActionProps = {
onPost: (model: string) => void;
};
function CustomAction({ onPost }: CustomActionProps) {
const { t } = useTranslation();
const [model, setModel] = useState("");
function post() {
const data = model.trim();
if (data === "") return;
onPost(data);
setModel("");
}
return (
setModel(e.target.value)}
onKeyDown={(e) => {
if (isEnter(e)) post();
}}
/>
{t("add")}
);
}
function validator(state: Channel): boolean {
return (
state.name.trim() !== "" &&
state.models.length > 0 &&
state.secret.trim() !== "" &&
state.endpoint.trim() !== ""
);
}
function handler(data: Channel): Channel {
data.models = data.models.filter((model) => model.trim() !== "");
data.name = data.name.trim();
data.secret = data.secret
.trim()
.split("\n")
.filter((line) => line.trim() !== "")
.join("\n");
data.endpoint = data.endpoint.trim();
data.endpoint.endsWith("/") && (data.endpoint = data.endpoint.slice(0, -1));
data.mapper = data.mapper
.trim()
.split("\n")
.filter((line) => {
if (line.trim() === "") return false;
const values = line.split(">");
return (
values.length === 2 &&
values[0].trim() !== "" &&
values[1].trim() !== ""
);
})
.join("\n");
data.group = data.group
? data.group.filter((group) => group.trim() !== "")
: [];
if (
data.proxy &&
data.proxy.proxy.trim() === "" &&
data.proxy.proxy_type !== 0
) {
data.proxy.proxy_type = 0;
}
return data;
}
type ChannelEditorProps = {
display: boolean;
id: number;
setEnabled: (enabled: boolean) => void;
edit: Channel;
dispatch: (action: any) => void;
data: Channel[];
};
function ChannelEditor({
display,
id,
edit,
dispatch,
setEnabled,
data,
}: ChannelEditorProps) {
const { t } = useTranslation();
const copy = useClipboard();
const info = useMemo(() => getChannelInfo(edit.type), [edit.type]);
const { channelModels } = useChannelModels();
const unusedModels = useMemo(() => {
return channelModels.filter(
(model) => !edit.models.includes(model) && model !== "",
);
}, [channelModels, edit.models]);
const enabled = useMemo(() => validator(edit), [edit]);
const [loading, setLoading] = useState(false);
function close(clear?: boolean) {
if (clear) dispatch({ type: "clear" });
setEnabled(false);
}
async function post() {
const data = handler(edit);
console.debug(`[channel] preflight channel data`, data);
const resp =
id === -1 ? await createChannel(data) : await updateChannel(id, data);
withNotify(t, resp as CommonResponse, true);
if (resp.status) {
close(true);
}
}
useEffectAsync(async () => {
if (id === -1) dispatch({ type: "clear" });
else {
setLoading(true);
const resp = await getChannel(id);
setLoading(false);
withNotify(t, resp as CommonResponse);
if (resp.data) dispatch({ type: "set", value: resp.data });
}
}, [id]);
return (
display && (
{loading && (
{t("admin.channels.loading")}
)}
dispatch({ type: "clear" })}
>
{t("admin.channels.new")}
{
const chan = data.find(
(channel) => channel.id.toString() === value,
);
if (!chan) return;
dispatch({ type: "import", value: chan });
console.debug(`[channel] import channel template: `, chan);
}}
>