1
0
forked from Leaf/amber-ui

Compare commits

...

41 Commits
main ... main

Author SHA1 Message Date
ivampiresp
29f650c83d Update manifests 2024-11-08 11:23:12 +00:00
af70f11e9b 改进 错误提示
All checks were successful
Build / build (push) Successful in 2m37s
2024-11-08 19:09:23 +08:00
07547899ef 增加 场景提示词设置 2024-11-08 18:51:57 +08:00
ivampiresp
9aba890b46 Update manifests 2024-11-07 08:13:07 +00:00
38b870438e rollback
All checks were successful
Build / build (push) Successful in 2m30s
2024-11-07 16:10:41 +08:00
ivampiresp
1f8238aabe Update manifests 2024-11-06 10:17:19 +00:00
1cf70e6bb4 改进 Token 刷新
All checks were successful
Build / build (push) Successful in 3m45s
2024-11-06 18:11:39 +08:00
cyq
b0f52ca420 Update manifests 2024-10-26 11:24:28 +00:00
kingc2022
25190adaaf 改进 UI 和删除聊天逻辑
All checks were successful
Build / build (push) Successful in 3m21s
2024-10-26 19:20:59 +08:00
ivampiresp
6cde2335f5 Update manifests 2024-10-17 14:08:12 +00:00
0e8b8aad9b 改进 登录逻辑
All checks were successful
Build / build (push) Successful in 1m31s
2024-10-17 22:06:36 +08:00
ivampiresp
99a1736d26 Update manifests 2024-10-16 13:02:05 +00:00
75d5f8db66 Merge branch 'main' of repo.leafdev.top:Leaf/amber-ui
All checks were successful
Build / build (push) Successful in 1m26s
2024-10-16 21:00:40 +08:00
0bcb10c810 改进 输入 2024-10-16 21:00:39 +08:00
ivampiresp
d2889a8bcc Update manifests 2024-10-16 12:18:52 +00:00
629ae9300f Merge branch 'main' of repo.leafdev.top:Leaf/amber-ui
All checks were successful
Build / build (push) Successful in 1m32s
2024-10-16 20:12:53 +08:00
5e5f7cd76e 改进 Markdown 渲染部分 2024-10-16 20:12:51 +08:00
ivampiresp
84f643f271 Update manifests 2024-10-14 17:03:57 +00:00
66290ea1d0 fix
All checks were successful
Build / build (push) Successful in 1m15s
2024-10-15 01:02:34 +08:00
bbfb41f706 增加 i18n 示例
Some checks failed
Build / build (push) Has been cancelled
2024-10-15 01:01:14 +08:00
ivampiresp
3f60f29806 Update manifests 2024-10-14 16:03:31 +00:00
fc604a9117 Merge branch 'main' of repo.leafdev.top:Leaf/amber-ui
All checks were successful
Build / build (push) Successful in 1m25s
2024-10-15 00:02:05 +08:00
74a35b1f11 改进 错误使用 console.log 弹出 2024-10-15 00:02:03 +08:00
ivampiresp
19e51a6bf3 Update manifests 2024-10-14 15:59:12 +00:00
1826dab66b 修复 内置工具判断问题
All checks were successful
Build / build (push) Successful in 1m25s
2024-10-14 23:57:37 +08:00
6d676afac6 Merge branch 'main' of repo.leafdev.top:Leaf/amber-ui
Some checks failed
Build / build (push) Has been cancelled
2024-10-14 23:56:41 +08:00
f3c7c40f1e 改进 Header 状态显示 2024-10-14 23:56:39 +08:00
a53f1b1710 移除 单独的网页浏览选项 2024-10-14 21:59:44 +08:00
ivampiresp
ea7a2aa883 Update manifests 2024-10-13 16:45:24 +00:00
3935e8c179 Merge branch 'main' of repo.leafdev.top:Leaf/amber-ui
All checks were successful
Build / build (push) Successful in 1m32s
2024-10-14 00:36:28 +08:00
65bbced587 Merge branch 'jwai-main' 2024-10-14 00:36:26 +08:00
ivampiresp
b133042d77 Update manifests 2024-10-13 16:19:24 +00:00
71cfbb722a Merge branch 'main' of repo.leafdev.top:Leaf/amber-ui
All checks were successful
Build / build (push) Successful in 1m32s
2024-10-14 00:06:11 +08:00
809405f3d9 重新生成 API 代码
增加 助理网络浏览和网络搜索开关
2024-10-14 00:06:09 +08:00
cyq
deab452156 Update manifests 2024-10-13 14:18:00 +00:00
kingc2022
7fc0dac915 Merge branch 'main' of repo.leafdev.top:Leaf/amber-ui
All checks were successful
Build / build (push) Successful in 1m31s
2024-10-13 22:16:07 +08:00
kingc2022
fb922e24ae 修改 首页 2024-10-13 22:07:56 +08:00
ivampiresp
79b7687b09 Update manifests 2024-10-13 13:15:44 +00:00
f46254b323 改进 首页
All checks were successful
Build / build (push) Successful in 1m27s
From Jwai
2024-10-13 21:14:13 +08:00
3d259b7331 更新 页面提示 2024-10-13 21:12:12 +08:00
81a5e85a3a 修复 登录问题 2024-10-13 21:08:02 +08:00
32 changed files with 3041 additions and 185 deletions

View File

@ -68,6 +68,9 @@
"onWatcherCleanup": true,
"useId": true,
"useModel": true,
"useTemplateRef": true
"useTemplateRef": true,
"DirectiveBinding": true,
"MaybeRef": true,
"MaybeRefOrGetter": true
}
}

View File

@ -9,6 +9,8 @@ definitions:
type: boolean
disable_memory:
type: boolean
disable_web_browsing:
type: boolean
enable_memory_for_assistant_api:
type: boolean
id:
@ -245,6 +247,24 @@ definitions:
user_id:
type: string
type: object
entity.ScenePrompt:
properties:
assistant:
$ref: '#/definitions/entity.Assistant'
assistant_id:
type: integer
created_at:
type: string
id:
description: Id schema.EntityId `gorm:"primarykey" json:"id,string"`
type: integer
label:
type: string
prompt:
type: string
updated_at:
type: string
type: object
entity.Tool:
properties:
api_key:
@ -267,8 +287,31 @@ definitions:
user_id:
type: string
type: object
page.PagedResult-entity_ChatMessageList:
properties:
count:
type: integer
data:
items:
$ref: '#/definitions/entity.ChatMessageList'
type: array
page:
description: 当前页码
type: integer
page_size:
description: 每页大小
type: integer
total_count:
description: 数据总条数
type: integer
total_pages:
description: 总页数
type: integer
type: object
page.PagedResult-schema_AssistantPublic:
properties:
count:
type: integer
data:
items:
$ref: '#/definitions/schema.AssistantPublic'
@ -374,6 +417,11 @@ definitions:
- true
- false
type: boolean
disable_web_browsing:
enum:
- true
- false
type: boolean
enable_memory_for_assistant_api:
enum:
- true
@ -488,6 +536,18 @@ definitions:
required:
- name
type: object
schema.CreateAssistantScenePromptRequest:
properties:
label:
maxLength: 20
type: string
prompt:
maxLength: 512
type: string
required:
- label
- prompt
type: object
schema.CurrentUserResponse:
properties:
ip:
@ -1074,6 +1134,104 @@ paths:
summary: 绑定资料库
tags:
- assistant
/api/v1/assistants/{id}/scene_prompts:
get:
consumes:
- application/json
parameters:
- in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/schema.ResponseBody'
- properties:
data:
items:
$ref: '#/definitions/entity.ScenePrompt'
type: array
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/schema.ResponseBody'
security:
- ApiKeyAuth: []
summary: 列出当前助理的场景 Prompt
tags:
- assistant
post:
consumes:
- application/json
parameters:
- in: path
name: id
required: true
type: integer
- description: CreateAssistantScenePromptRequest
in: body
name: CreateAssistantScenePromptRequest
required: true
schema:
$ref: '#/definitions/schema.CreateAssistantScenePromptRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/schema.ResponseBody'
- properties:
data:
$ref: '#/definitions/entity.ScenePrompt'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/schema.ResponseBody'
security:
- ApiKeyAuth: []
summary: 创建场景 Prompt
tags:
- assistant
/api/v1/assistants/{id}/scene_prompts/{scene_id}:
delete:
consumes:
- application/json
parameters:
- in: path
name: id
required: true
type: integer
- in: path
name: scene_id
required: true
type: integer
produces:
- application/json
responses:
"204":
description: No Content
"404":
description: Not Found
schema:
$ref: '#/definitions/schema.ResponseBody'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/schema.ResponseBody'
security:
- ApiKeyAuth: []
summary: 删除场景 Prompt
tags:
- assistant
/api/v1/assistants/{id}/tools:
get:
consumes:
@ -1913,6 +2071,48 @@ paths:
summary: 添加聊天记录
tags:
- chat_message
/api/v1/chats/{id}/messages/paginate:
get:
consumes:
- application/json
description: 获取一个对话的所有聊天记录
parameters:
- in: path
name: id
required: true
type: integer
- in: query
name: page
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/schema.ResponseBody'
- properties:
data:
$ref: '#/definitions/page.PagedResult-entity_ChatMessageList'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/schema.ResponseBody'
"404":
description: Not Found
schema:
$ref: '#/definitions/schema.ResponseBody'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/schema.ResponseBody'
security:
- ApiKeyAuth: []
summary: 分页获取聊天记录
tags:
- chat_message
/api/v1/files/download/{hash}:
get:
consumes:

View File

@ -17,7 +17,7 @@ spec:
- name: leaf
containers:
- name: amber
image: leafdev.top/leaf/amber-ui:dbdc39f
image: leafdev.top/leaf/amber-ui:af70f11
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80

View File

@ -11,18 +11,19 @@
"gen": "openapi-generator-cli generate -i ./api/swagger.yaml -g typescript-axios -o ./src/api"
},
"dependencies": {
"@kangc/v-md-editor": "^2.3.18",
"@iktakahiro/markdown-it-katex": "^4.0.1",
"@notable/html2markdown": "^2.0.3",
"@traptitech/markdown-it-katex": "^3.6.0",
"animate.css": "^4.1.1",
"axios": "^1.7.7",
"highlight.js": "^11.10.0",
"js-base64": "^3.7.7",
"katex": "^0.16.11",
"lottie-web": "^5.12.2",
"markdown-it": "^14.1.0",
"pinia": "^2.2.2",
"pinia-plugin-persistedstate": "^4.0.0",
"vooks": "^0.2.12"
"vooks": "^0.2.12",
"vue-i18n": "10"
},
"devDependencies": {
"@babel/types": "^7.18.10",
@ -47,6 +48,7 @@
"naive-ui": "^2.39.0",
"postcss": "^8.4.45",
"prettier": "^3.3.3",
"sass-embedded": "^1.79.5",
"tailwindcss": "^3.4.10",
"typescript": "^5.5.2",
"unplugin-auto-import": "^0.18.2",

View File

@ -147,6 +147,68 @@ export interface ApiV1AssistantsIdKeysPost200Response {
*/
'success'?: boolean;
}
/**
*
* @export
* @interface ApiV1AssistantsIdScenePromptsGet200Response
*/
export interface ApiV1AssistantsIdScenePromptsGet200Response {
/**
*
* @type {Array<EntityScenePrompt>}
* @memberof ApiV1AssistantsIdScenePromptsGet200Response
*/
'data'?: Array<EntityScenePrompt>;
/**
*
* @type {string}
* @memberof ApiV1AssistantsIdScenePromptsGet200Response
*/
'error'?: string;
/**
*
* @type {string}
* @memberof ApiV1AssistantsIdScenePromptsGet200Response
*/
'message'?: string;
/**
*
* @type {boolean}
* @memberof ApiV1AssistantsIdScenePromptsGet200Response
*/
'success'?: boolean;
}
/**
*
* @export
* @interface ApiV1AssistantsIdScenePromptsPost200Response
*/
export interface ApiV1AssistantsIdScenePromptsPost200Response {
/**
*
* @type {EntityScenePrompt}
* @memberof ApiV1AssistantsIdScenePromptsPost200Response
*/
'data'?: EntityScenePrompt;
/**
*
* @type {string}
* @memberof ApiV1AssistantsIdScenePromptsPost200Response
*/
'error'?: string;
/**
*
* @type {string}
* @memberof ApiV1AssistantsIdScenePromptsPost200Response
*/
'message'?: string;
/**
*
* @type {boolean}
* @memberof ApiV1AssistantsIdScenePromptsPost200Response
*/
'success'?: boolean;
}
/**
*
* @export
@ -476,6 +538,37 @@ export interface ApiV1ChatsIdMessagesGet200Response {
*/
'success'?: boolean;
}
/**
*
* @export
* @interface ApiV1ChatsIdMessagesPaginateGet200Response
*/
export interface ApiV1ChatsIdMessagesPaginateGet200Response {
/**
*
* @type {PagePagedResultEntityChatMessageList}
* @memberof ApiV1ChatsIdMessagesPaginateGet200Response
*/
'data'?: PagePagedResultEntityChatMessageList;
/**
*
* @type {string}
* @memberof ApiV1ChatsIdMessagesPaginateGet200Response
*/
'error'?: string;
/**
*
* @type {string}
* @memberof ApiV1ChatsIdMessagesPaginateGet200Response
*/
'message'?: string;
/**
*
* @type {boolean}
* @memberof ApiV1ChatsIdMessagesPaginateGet200Response
*/
'success'?: boolean;
}
/**
*
* @export
@ -754,6 +847,12 @@ export interface EntityAssistant {
* @memberof EntityAssistant
*/
'disable_memory'?: boolean;
/**
*
* @type {boolean}
* @memberof EntityAssistant
*/
'disable_web_browsing'?: boolean;
/**
*
* @type {boolean}
@ -1377,6 +1476,55 @@ export interface EntityMemory {
*/
'user_id'?: string;
}
/**
*
* @export
* @interface EntityScenePrompt
*/
export interface EntityScenePrompt {
/**
*
* @type {EntityAssistant}
* @memberof EntityScenePrompt
*/
'assistant'?: EntityAssistant;
/**
*
* @type {number}
* @memberof EntityScenePrompt
*/
'assistant_id'?: number;
/**
*
* @type {string}
* @memberof EntityScenePrompt
*/
'created_at'?: string;
/**
* Id schema.EntityId `gorm:\"primarykey\" json:\"id,string\"`
* @type {number}
* @memberof EntityScenePrompt
*/
'id'?: number;
/**
*
* @type {string}
* @memberof EntityScenePrompt
*/
'label'?: string;
/**
*
* @type {string}
* @memberof EntityScenePrompt
*/
'prompt'?: string;
/**
*
* @type {string}
* @memberof EntityScenePrompt
*/
'updated_at'?: string;
}
/**
*
* @export
@ -1438,12 +1586,61 @@ export interface EntityTool {
*/
'user_id'?: string;
}
/**
*
* @export
* @interface PagePagedResultEntityChatMessageList
*/
export interface PagePagedResultEntityChatMessageList {
/**
*
* @type {number}
* @memberof PagePagedResultEntityChatMessageList
*/
'count'?: number;
/**
*
* @type {Array<EntityChatMessageList>}
* @memberof PagePagedResultEntityChatMessageList
*/
'data'?: Array<EntityChatMessageList>;
/**
*
* @type {number}
* @memberof PagePagedResultEntityChatMessageList
*/
'page'?: number;
/**
*
* @type {number}
* @memberof PagePagedResultEntityChatMessageList
*/
'page_size'?: number;
/**
*
* @type {number}
* @memberof PagePagedResultEntityChatMessageList
*/
'total_count'?: number;
/**
*
* @type {number}
* @memberof PagePagedResultEntityChatMessageList
*/
'total_pages'?: number;
}
/**
*
* @export
* @interface PagePagedResultSchemaAssistantPublic
*/
export interface PagePagedResultSchemaAssistantPublic {
/**
*
* @type {number}
* @memberof PagePagedResultSchemaAssistantPublic
*/
'count'?: number;
/**
*
* @type {Array<SchemaAssistantPublic>}
@ -1618,6 +1815,12 @@ export interface SchemaAssistantUpdateRequest {
* @memberof SchemaAssistantUpdateRequest
*/
'disable_memory'?: boolean;
/**
*
* @type {boolean}
* @memberof SchemaAssistantUpdateRequest
*/
'disable_web_browsing'?: boolean;
/**
*
* @type {boolean}
@ -1818,6 +2021,25 @@ export interface SchemaChatUpdateRequest {
*/
'prompt'?: string;
}
/**
*
* @export
* @interface SchemaCreateAssistantScenePromptRequest
*/
export interface SchemaCreateAssistantScenePromptRequest {
/**
*
* @type {string}
* @memberof SchemaCreateAssistantScenePromptRequest
*/
'label': string;
/**
*
* @type {string}
* @memberof SchemaCreateAssistantScenePromptRequest
*/
'prompt': string;
}
/**
*
* @export
@ -2769,6 +2991,127 @@ export const AssistantApiAxiosParamCreator = function (configuration?: Configura
options: localVarRequestOptions,
};
},
/**
*
* @summary Prompt
* @param {number} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiV1AssistantsIdScenePromptsGet: async (id: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('apiV1AssistantsIdScenePromptsGet', 'id', id)
const localVarPath = `/api/v1/assistants/{id}/scene_prompts`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication ApiKeyAuth required
await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary Prompt
* @param {number} id
* @param {SchemaCreateAssistantScenePromptRequest} createAssistantScenePromptRequest CreateAssistantScenePromptRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiV1AssistantsIdScenePromptsPost: async (id: number, createAssistantScenePromptRequest: SchemaCreateAssistantScenePromptRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('apiV1AssistantsIdScenePromptsPost', 'id', id)
// verify required parameter 'createAssistantScenePromptRequest' is not null or undefined
assertParamExists('apiV1AssistantsIdScenePromptsPost', 'createAssistantScenePromptRequest', createAssistantScenePromptRequest)
const localVarPath = `/api/v1/assistants/{id}/scene_prompts`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication ApiKeyAuth required
await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(createAssistantScenePromptRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary Prompt
* @param {number} id
* @param {number} sceneId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiV1AssistantsIdScenePromptsSceneIdDelete: async (id: number, sceneId: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('apiV1AssistantsIdScenePromptsSceneIdDelete', 'id', id)
// verify required parameter 'sceneId' is not null or undefined
assertParamExists('apiV1AssistantsIdScenePromptsSceneIdDelete', 'sceneId', sceneId)
const localVarPath = `/api/v1/assistants/{id}/scene_prompts/{scene_id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)))
.replace(`{${"scene_id"}}`, encodeURIComponent(String(sceneId)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication ApiKeyAuth required
await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary Assistant Tool
@ -3181,6 +3524,47 @@ export const AssistantApiFp = function(configuration?: Configuration) {
const localVarOperationServerBasePath = operationServerMap['AssistantApi.apiV1AssistantsIdPut']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @summary Prompt
* @param {number} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiV1AssistantsIdScenePromptsGet(id: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ApiV1AssistantsIdScenePromptsGet200Response>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.apiV1AssistantsIdScenePromptsGet(id, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['AssistantApi.apiV1AssistantsIdScenePromptsGet']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @summary Prompt
* @param {number} id
* @param {SchemaCreateAssistantScenePromptRequest} createAssistantScenePromptRequest CreateAssistantScenePromptRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiV1AssistantsIdScenePromptsPost(id: number, createAssistantScenePromptRequest: SchemaCreateAssistantScenePromptRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ApiV1AssistantsIdScenePromptsPost200Response>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.apiV1AssistantsIdScenePromptsPost(id, createAssistantScenePromptRequest, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['AssistantApi.apiV1AssistantsIdScenePromptsPost']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @summary Prompt
* @param {number} id
* @param {number} sceneId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiV1AssistantsIdScenePromptsSceneIdDelete(id: number, sceneId: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.apiV1AssistantsIdScenePromptsSceneIdDelete(id, sceneId, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['AssistantApi.apiV1AssistantsIdScenePromptsSceneIdDelete']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @summary Assistant Tool
@ -3386,6 +3770,38 @@ export const AssistantApiFactory = function (configuration?: Configuration, base
apiV1AssistantsIdPut(id: number, assistantUpdateRequest: SchemaAssistantUpdateRequest, options?: RawAxiosRequestConfig): AxiosPromise<ApiV1AssistantsPost200Response> {
return localVarFp.apiV1AssistantsIdPut(id, assistantUpdateRequest, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Prompt
* @param {number} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiV1AssistantsIdScenePromptsGet(id: number, options?: RawAxiosRequestConfig): AxiosPromise<ApiV1AssistantsIdScenePromptsGet200Response> {
return localVarFp.apiV1AssistantsIdScenePromptsGet(id, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Prompt
* @param {number} id
* @param {SchemaCreateAssistantScenePromptRequest} createAssistantScenePromptRequest CreateAssistantScenePromptRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiV1AssistantsIdScenePromptsPost(id: number, createAssistantScenePromptRequest: SchemaCreateAssistantScenePromptRequest, options?: RawAxiosRequestConfig): AxiosPromise<ApiV1AssistantsIdScenePromptsPost200Response> {
return localVarFp.apiV1AssistantsIdScenePromptsPost(id, createAssistantScenePromptRequest, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Prompt
* @param {number} id
* @param {number} sceneId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiV1AssistantsIdScenePromptsSceneIdDelete(id: number, sceneId: number, options?: RawAxiosRequestConfig): AxiosPromise<void> {
return localVarFp.apiV1AssistantsIdScenePromptsSceneIdDelete(id, sceneId, options).then((request) => request(axios, basePath));
},
/**
*
* @summary Assistant Tool
@ -3590,6 +4006,44 @@ export class AssistantApi extends BaseAPI {
return AssistantApiFp(this.configuration).apiV1AssistantsIdPut(id, assistantUpdateRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Prompt
* @param {number} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssistantApi
*/
public apiV1AssistantsIdScenePromptsGet(id: number, options?: RawAxiosRequestConfig) {
return AssistantApiFp(this.configuration).apiV1AssistantsIdScenePromptsGet(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Prompt
* @param {number} id
* @param {SchemaCreateAssistantScenePromptRequest} createAssistantScenePromptRequest CreateAssistantScenePromptRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssistantApi
*/
public apiV1AssistantsIdScenePromptsPost(id: number, createAssistantScenePromptRequest: SchemaCreateAssistantScenePromptRequest, options?: RawAxiosRequestConfig) {
return AssistantApiFp(this.configuration).apiV1AssistantsIdScenePromptsPost(id, createAssistantScenePromptRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Prompt
* @param {number} id
* @param {number} sceneId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssistantApi
*/
public apiV1AssistantsIdScenePromptsSceneIdDelete(id: number, sceneId: number, options?: RawAxiosRequestConfig) {
return AssistantApiFp(this.configuration).apiV1AssistantsIdScenePromptsSceneIdDelete(id, sceneId, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary Assistant Tool
@ -4286,6 +4740,48 @@ export const ChatMessageApiAxiosParamCreator = function (configuration?: Configu
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @summary
* @param {number} id
* @param {number} [page]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiV1ChatsIdMessagesPaginateGet: async (id: number, page?: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('apiV1ChatsIdMessagesPaginateGet', 'id', id)
const localVarPath = `/api/v1/chats/{id}/messages/paginate`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication ApiKeyAuth required
await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration)
if (page !== undefined) {
localVarQueryParameter['page'] = page;
}
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@ -4427,6 +4923,20 @@ export const ChatMessageApiFp = function(configuration?: Configuration) {
const localVarOperationServerBasePath = operationServerMap['ChatMessageApi.apiV1ChatsIdMessagesGet']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @summary
* @param {number} id
* @param {number} [page]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async apiV1ChatsIdMessagesPaginateGet(id: number, page?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ApiV1ChatsIdMessagesPaginateGet200Response>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.apiV1ChatsIdMessagesPaginateGet(id, page, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['ChatMessageApi.apiV1ChatsIdMessagesPaginateGet']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @summary
@ -4496,6 +5006,17 @@ export const ChatMessageApiFactory = function (configuration?: Configuration, ba
apiV1ChatsIdMessagesGet(id: number, options?: RawAxiosRequestConfig): AxiosPromise<ApiV1ChatsIdMessagesGet200Response> {
return localVarFp.apiV1ChatsIdMessagesGet(id, options).then((request) => request(axios, basePath));
},
/**
*
* @summary
* @param {number} id
* @param {number} [page]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
apiV1ChatsIdMessagesPaginateGet(id: number, page?: number, options?: RawAxiosRequestConfig): AxiosPromise<ApiV1ChatsIdMessagesPaginateGet200Response> {
return localVarFp.apiV1ChatsIdMessagesPaginateGet(id, page, options).then((request) => request(axios, basePath));
},
/**
*
* @summary
@ -4565,6 +5086,19 @@ export class ChatMessageApi extends BaseAPI {
return ChatMessageApiFp(this.configuration).apiV1ChatsIdMessagesGet(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary
* @param {number} id
* @param {number} [page]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ChatMessageApi
*/
public apiV1ChatsIdMessagesPaginateGet(id: number, page?: number, options?: RawAxiosRequestConfig) {
return ChatMessageApiFp(this.configuration).apiV1ChatsIdMessagesPaginateGet(id, page, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @summary

2
src/components.d.ts vendored
View File

@ -9,7 +9,6 @@ declare module 'vue' {
export interface GlobalComponents {
AccountSettings: typeof import('./components/settings/AccountSettings.vue')['default']
AssistantMenu: typeof import('./components/AssistantMenu.vue')['default']
AssistantPublistSettings: typeof import('./components/settings/AssistantPublistSettings.vue')['default']
AssistantSettings: typeof import('./components/settings/AssistantSettings.vue')['default']
Chat: typeof import('./components/chat/Chat.vue')['default']
ChatMenu: typeof import('./components/ChatMenu.vue')['default']
@ -18,6 +17,7 @@ declare module 'vue' {
LeftSettings: typeof import('./components/settings/LeftSettings.vue')['default']
LibrarySettings: typeof import('./components/settings/LibrarySettings.vue')['default']
Lottie: typeof import('./components/Lottie.vue')['default']
Markdown: typeof import('./components/markdown/index.vue')['default']
Mask: typeof import('./components/Mask.vue')['default']
MemorySettings: typeof import('./components/settings/MemorySettings.vue')['default']
Menu: typeof import('./components/Menu.vue')['default']

View File

@ -27,8 +27,8 @@
<n-text depth="3">
我们有一些预设提示词您可以点击来一键输入
</n-text>
<n-scrollbar style="max-height: 400px">
<n-grid cols="1 s:2 l:3" responsive="screen">
<n-scrollbar style="max-height: 400px" class="mt-3">
<n-grid cols="1 m:2 l:3" responsive="screen">
<n-gi v-for="p in prompts" class="rounded-lg p-2">
<n-card
:title="p.act"
@ -60,7 +60,7 @@
</n-scrollbar>
</div>
<div class="min-w-full pt-2 relative " style="max-height: 120px">
<div class="min-w-full pt-2 relative" style="max-height: 120px">
<div class="relative bottom-0 pr-2 pl-2 min-w-full" ref="textContainer">
<div
class="mx-auto w-2xl max-w-2xl text-center mb-3 animate__animated animate__pulse text-lg"
@ -674,11 +674,36 @@ function streamChat(streamId: String, redirect = false) {
});
break;
case "tool_calling":
if (data.tool_call_message.function_name.startsWith("builtin_")) {
if (data.tool_call_message.function_name === "builtin_browser") {
let url = data.tool_call_message.args.query_or_url;
// URL
if (url.startsWith("http")) {
chatStore.toolName = "正在浏览 " + url;
} else {
chatStore.toolName = "正在搜索 " + url;
}
} else if (data.tool_call_message.function_name === "builtin_calculator") {
chatStore.toolName = "正在计算";
} else if (
data.tool_call_message.function_name === "builtin_generate_image"
) {
chatStore.toolName = "生成图片中";
} else if (
data.tool_call_message.function_name === "builtin_describe_image"
) {
chatStore.toolName = "正在理解图片";
}
} else {
chatStore.toolName =
"正在执行 " +
data.tool_call_message.tool_name +
" 中的 " +
data.tool_call_message.function_name;
}
// toolCalling.value = true;
chatStore.toolName =
data.tool_call_message.tool_name +
" 中的 " +
data.tool_call_message.function_name;
// toolName.value =
// data.tool_call_message.tool_name +
// " " +

View File

@ -36,7 +36,7 @@
</n-thing>
</n-list-item>
</n-list>
<div v-else>
<div v-else class="mt-8">
<n-result
status="404"
title="你还没有对话"
@ -99,6 +99,9 @@ const deleteChat = async (chatId: number) => {
onPositiveClick: async () => {
await getApi().Chat.apiV1ChatsIdDelete(chatId);
await getChats();
if (parseInt(router.currentRoute.value.params.id) === chatId) {
router.push("/")
}
},
});
};

View File

@ -35,7 +35,14 @@
</div>
</n-flex>
</div>
<div v-else-if="(message.role === 'user' || message.role === 'user_hide' || message.role === 'user_later') && message.content">
<div
v-else-if="
(message.role === 'user' ||
message.role === 'user_hide' ||
message.role === 'user_later') &&
message.content
"
>
<!-- 用户消息 -->
<n-flex justify="end">
<div class="flex items-center flex-nowrap">
@ -51,11 +58,14 @@
{{ userStore.user.name }}
</n-divider>
</div>
<div
<!-- <div
v-if="mdInited"
class="break-all break-words markdown-body"
v-html="mdIt.render(message.content)"
></div>
></div> -->
<div>
<Markdown :typing="false" :content="message.content" />
</div>
</div>
<div class="relative h-full">
@ -103,11 +113,10 @@
{{ message.assistant?.name }}
</n-divider>
</div>
<div
v-if="mdInited"
class="break-all break-words markdown-body"
v-html="mdIt.render(message.content)"
></div>
<div>
<Markdown :typing="false" :content="message.content" />
</div>
</div>
<!-- <div v-html="mdIt.render(message.content)"></div> -->
@ -126,50 +135,9 @@ import { Ref } from "vue";
import { EntityChatMessage } from "@/api";
import { useUserStore } from "@/stores/user";
import leaflowPng from "@/assets/images/leaflow.png";
import markdownKatex from "@traptitech/markdown-it-katex";
import markdownIt from "markdown-it";
// highlightjs
import hljs from "highlight.js";
import Markdown from "@/components/markdown/index.vue";
import config from "@/config/config";
const mdIt = markdownIt();
const mdInited = ref(true);
const unsupportedLanguages = ["assembly", "blade", "vue"];
mdIt.options.highlight = function (str: string, lang: string) {
// TODO:
lang = "text"
if (!lang || unsupportedLanguages.includes(lang)) {
// return str;
lang = "text"
}
return hljs.highlight(str, { language: lang }).value;
};
mdIt.use(markdownKatex, {
throwOnError: false,
errorColor: "#cc0000",
output: "html",
});
// async function initMD() {
// mdIt.use(
// await Shiki({
// themes: {
// light: "vitesse-light",
// dark: "vitesse-dark",
// },
// })
// );
// mdIt.use(markdownKatex, {
// throwOnError: false,
// errorColor: "#cc0000",
// output: "html",
// });
// mdInited.value = true;
// }
const userStore = useUserStore();
const props = defineProps({
@ -182,7 +150,4 @@ const props = defineProps({
const chat_messages = toRef(props, "chat_messages") as Ref<EntityChatMessage[]>;
const fileBaseUrl = config.backend + "/api/v1/files";
onMounted(() => {
// initMD();
});
</script>

View File

@ -0,0 +1,17 @@
import type { PluginWithOptions } from 'markdown-it'
export interface APluginOptions {}
export const aPlugin: PluginWithOptions<APluginOptions> = (
md,
options = {}
): void => {
md.renderer.rules.link_open = (tokens, idx, options, env, slf) => {
const aIndex = tokens[idx].attrIndex('href')
if (aIndex !== -1) {
tokens[idx].attrs.push(['target', '_blank'])
}
return slf.renderToken(tokens, idx, options)
}
}

View File

@ -0,0 +1,52 @@
import type { PluginWithOptions } from 'markdown-it'
import { resolveLanguage } from './resolveLanguage.js'
import { resolveLineNumbers } from './resolveLineNumbers.js'
export interface CodePluginOptions {
lineNumbers?: boolean | number
}
export const codePlugin: PluginWithOptions<CodePluginOptions> = (
md,
{ lineNumbers = true } = {}
): void => {
md.renderer.rules.fence = (tokens, idx, options, env, slf) => {
const token = tokens[idx]
const info = token.info ? md.utils.unescapeAll(token.info).trim() : ''
const language = resolveLanguage(info)
const languageClass = `${options.langPrefix}${language.name}`
const code =
options.highlight?.(token.content, language.name, '') ||
md.utils.escapeHtml(token.content)
token.attrJoin('class', languageClass)
let result = code.startsWith('<pre')
? code
: `<pre${slf.renderAttrs(token)}><code>${code}</code></pre>`
result = `<div data-ext="${language.ext}" v-pre class="code-copy-line">
<div class="code-copy-btn"></div>
</div>${result}`
const lines = code.split('\n').slice(0, -1)
const useLineNumbers =
resolveLineNumbers(info) ??
(typeof lineNumbers === 'number'
? lines.length >= lineNumbers
: lineNumbers)
if (useLineNumbers) {
const lineNumbersCode = lines
.map(() => `<div class="line-number"></div>`)
.join('')
result = `<div class="line-numbers" aria-hidden="true">${lineNumbersCode}</div>${result}`
}
result = `<div><div class="${languageClass}${
useLineNumbers ? ' line-numbers-mode' : ''
}"><div class="pre-code-scroll">${result}</div></div></div>`
return result
}
}

View File

@ -0,0 +1,105 @@
/**
* Language type for syntax highlight
*/
export interface HighlightLanguage {
/**
* Name of the language
*
* The name to be used for the class name,
* e.g. `class="language-typescript"`
*/
name: string
/**
* Extension of the language
*
* The file extension, which will be used for the
* class name, e.g. `class="ext-ts"`
*/
ext: string
/**
* Aliases that point to this language
*
* Do not conflict with other languages
*/
aliases: string[]
}
export const languageBash: HighlightLanguage = {
name: 'bash',
ext: 'sh',
aliases: ['bash', 'sh', 'shell', 'zsh']
}
export const languageCsharp: HighlightLanguage = {
name: 'csharp',
ext: 'cs',
aliases: ['cs', 'csharp']
}
export const languageDocker: HighlightLanguage = {
name: 'docker',
ext: 'docker',
aliases: ['docker', 'dockerfile']
}
export const languageFsharp: HighlightLanguage = {
name: 'fsharp',
ext: 'fs',
aliases: ['fs', 'fsharp']
}
export const languageJavascript: HighlightLanguage = {
name: 'javascript',
ext: 'js',
aliases: ['javascript', 'js']
}
export const languageKotlin: HighlightLanguage = {
name: 'kotlin',
ext: 'kt',
aliases: ['kotlin', 'kt']
}
export const languageMarkdown: HighlightLanguage = {
name: 'markdown',
ext: 'md',
aliases: ['markdown', 'md']
}
export const languagePython: HighlightLanguage = {
name: 'python',
ext: 'py',
aliases: ['py', 'python']
}
export const languageRuby: HighlightLanguage = {
name: 'ruby',
ext: 'rb',
aliases: ['rb', 'ruby']
}
export const languageRust: HighlightLanguage = {
name: 'rust',
ext: 'rs',
aliases: ['rs', 'rust']
}
export const languageStylus: HighlightLanguage = {
name: 'stylus',
ext: 'styl',
aliases: ['styl', 'stylus']
}
export const languageTypescript: HighlightLanguage = {
name: 'typescript',
ext: 'ts',
aliases: ['ts', 'typescript']
}
export const languageYaml: HighlightLanguage = {
name: 'yaml',
ext: 'yml',
aliases: ['yaml', 'yml']
}

View File

@ -0,0 +1,46 @@
import * as languages from './languages.js'
import type { HighlightLanguage } from './languages.js'
type LanguagesMap = Record<string, HighlightLanguage>
/**
* A key-value map to get language info from alias
*
* - key: alias
* - value: language
*/
let languagesMap: LanguagesMap
/**
* Lazy generate languages map
*/
const getLanguagesMap = (): LanguagesMap => {
if (!languagesMap) {
languagesMap = Object.values(languages).reduce((result, item) => {
item.aliases.forEach((alias) => {
result[alias] = item
})
return result
}, {} as LanguagesMap)
}
return languagesMap
}
/**
* Resolve language for highlight from token info
*/
export const resolveLanguage = (info: string): HighlightLanguage => {
// get user-defined language alias
const alias = info.match(/^([^ :[{]+)/)?.[1] || ''
// if the alias does not have a match in the map
// fallback to the alias itself
return (
getLanguagesMap()[alias] ?? {
name: alias,
ext: alias,
aliases: [alias]
}
)
}

View File

@ -0,0 +1,14 @@
/**
* Resolve the `:line-numbers` / `:no-line-numbers` mark from token info
*/
export const resolveLineNumbers = (info: string): boolean | null => {
if (/:line-numbers\b/.test(info)) {
return true
}
if (/:no-line-numbers\b/.test(info)) {
return false
}
return null
}

View File

@ -0,0 +1,76 @@
import type { PluginWithOptions } from 'markdown-it'
const defaultMarker = '#'
interface CustomLinkPluginOptions {
marker: string
}
export const customLinkPlugin: PluginWithOptions<
Partial<CustomLinkPluginOptions>
> = (md, options = {}): void => {
const marker = options.marker || defaultMarker
md.block.ruler.before(
'paragraph',
'custom_link',
function (state, startLine, endLine, silent) {
const pos = state.bMarks[startLine] + state.tShift[startLine]
const max = state.eMarks[startLine]
if (pos >= max) {
return false
}
if (silent) {
return true
}
const text = state.src.substring(pos, max)
const start = text.indexOf(marker)
const end = text.lastIndexOf(marker)
if (start < 0 || end < 0 || start == end) {
return false
}
//#...#规则前面的内容
const startContent = text.substring(0, start)
//#...#规则后面的内容
const endContent = text.substring(end + 1)
//#...#规则中间的内容
const content = text.substring(start + 1, end)
//插入<div>
const token_div_o = state.push('div_open', 'div', 1)
token_div_o.map = [startLine, state.line]
//插入#...#规则前面的内容
const token_s = state.push('inline', '', 0)
token_s.content = startContent
token_s.children = []
//插入<a class="markdown-custom-link">
const token_a_o = state.push('link_open', 'a', 1)
token_a_o.attrs = [['class', 'markdown-custom-link']]
token_a_o.map = [startLine, state.line]
//插入#...#规则中间的内容
const token = state.push('inline', '', 0)
token.content = content
token.children = []
//闭合a标签
state.push('link_close', 'a', -1)
const token_e = state.push('inline', '', 0)
//插入#...#规则后面的内容
token_e.content = endContent
token_e.children = []
//闭合div标签
state.push('div_close', 'div', -1)
state.line = startLine + 1
return true
},
{
alt: ['paragraph', 'reference', 'blockquote']
}
)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,166 @@
<template>
<div class="markdown-it-container">
<div
ref="markdownBodyRef"
class="markdown-body"
@click="handleClick($event)"
v-html="result"
/>
<div v-if="typing" class="markdown-typing" :style="cursorPosition"/>
</div>
</template>
<script setup lang="ts">
import MarkdownIt from 'markdown-it'
import hljs from 'highlight.js'
// import 'highlight.js/styles/atom-one-dark.css'
// import './github-markdown.css'
// @ts-ignore
import markdownItMath from '@iktakahiro/markdown-it-katex'
import type {CodePluginOptions} from './codePlugin'
import {codePlugin} from './codePlugin'
import {customLinkPlugin} from './customLink'
import {aPlugin} from './aPlugin'
interface Options extends Partial<MarkdownIt.Options> {
lineNumbers?: boolean
}
const props = withDefaults(
defineProps<{
content: string
html?: boolean
breaks?: boolean
linkify?: boolean
typographer?: boolean
//
lineNumbers?: boolean
//
typing: boolean
}>(),
{
content: '',
html: true,
breaks: true,
typographer: true,
linkify: true,
lineNumbers: true,
typing: false
}
)
const emit = defineEmits<{
(event: 'click-custom-link', value: string): void
}>()
const result = ref('')
const markdownBodyRef = shallowRef<HTMLDivElement>()
const createMarkdown = (options: Options) => {
const md = new MarkdownIt({
...options,
langPrefix: 'language-',
highlight(str: any, lang: any) {
try {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(lang, str, true).value
}
return hljs.highlightAuto(str).value
} catch (error) {
return str
}
}
})
md.use<CodePluginOptions>(codePlugin, {
lineNumbers: options.lineNumbers
})
md.use(markdownItMath, {
output: 'mathml'
})
md.use(aPlugin)
md.use(customLinkPlugin)
return md
}
let md: MarkdownIt
const findLastTextNode = (parent: Node): Node | undefined => {
const children = parent.childNodes
for (let i = children.length - 1; i >= 0; i--) {
const node = children[i]
if (node.nodeType === Node.TEXT_NODE && /\S/.test(node.nodeValue!)) {
node.nodeValue?.replace(/\S+$/, '')
return node
} else if (node.nodeType === Node.ELEMENT_NODE) {
const lastNode = findLastTextNode(node)
if (lastNode) {
return lastNode
}
}
}
}
const cursorPosition = reactive({
top: 'auto',
left: 'auto'
})
const updateCursor = () => {
if (markdownBodyRef.value) {
const lastTextNode = findLastTextNode(markdownBodyRef.value)
const textNode = document.createTextNode('\u200B')
if (lastTextNode) {
lastTextNode.parentElement?.appendChild(textNode)
} else {
markdownBodyRef.value?.appendChild(textNode)
}
const range = document.createRange()
range.setStart(textNode, 0)
range.setEnd(textNode, 0)
const textRect = range.getBoundingClientRect()
const markdownBodyRect = markdownBodyRef.value?.getBoundingClientRect()
cursorPosition.left = `${textRect.left - markdownBodyRect.left}px`
cursorPosition.top = `${textRect.top - markdownBodyRect.top}px`
textNode.remove()
}
}
const preprocessContent = (content: string) => {
return content.replace(/\n(#.*#)/g, '\n\n$1')
}
watchEffect(() => {
md = createMarkdown({
html: props.html,
breaks: props.breaks,
typographer: props.typographer,
linkify: props.linkify,
lineNumbers: props.lineNumbers
})
result.value = md.render(preprocessContent(props.content))
})
watch(
() => props.content,
async (value) => {
result.value = md?.render(preprocessContent(value))
await nextTick()
updateCursor()
},
{
immediate: true
}
)
const handleClick = async (e: any) => {
console.log(e.target)
const target: HTMLElement = e.target
if (target.className === 'code-copy-btn') {
const text = e.target.parentElement.nextElementSibling.textContent
// copy text to clipboard
await navigator.clipboard.writeText(text)
}
if (target.className === 'markdown-custom-link') {
emit('click-custom-link', target.textContent!)
}
}
</script>
<style lang="scss">
@use 'markdown.scss';
</style>

View File

@ -0,0 +1,212 @@
$line-height: 1.375;
:root {
--code-bg-color: #35373f;
--code-hl-bg-color: rgba(0, 0, 0, 0.66);
--code-ln-color: #6e6e7f;
--code-ln-wrapper-width: 3.5rem;
}
@keyframes blink {
from,
to {
opacity: 0;
}
50% {
opacity: 1;
}
}
.markdown-it-container {
.markdown-body {
background-color: transparent;
font-size: 15px;
.markdown-custom-link {
color: var(--el-color-primary);
cursor: pointer;
}
}
position: relative;
.markdown-typing {
position: absolute;
display: inline-block;
content: '';
width: 5px;
height: 14px;
transform: translate(4px, 2px) scaleY(1.3);
color: #1a202c;
background-color: currentColor;
animation: blink 0.6s infinite;
}
ul {
list-style: disc;
}
ol {
list-style: decimal;
}
code {
padding: 0.25rem;
margin: 0;
font-size: 0.85em;
border-radius: 3px;
}
code[class*='language-'],
pre[class*='language-'] {
color: #ccc;
background: none;
font-size: 1em;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*='language-'] {
padding: 20px 0;
margin: 0;
overflow: auto;
}
:not(pre) > code[class*='language-'],
pre[class*='language-'] {
font-size: 0.85em;
background: #35373f;
}
/* Inline code */
:not(pre) > code[class*='language-'] {
border-radius: 0.3em;
white-space: normal;
}
pre,
pre[class*='language-'] {
line-height: $line-height;
border-radius: 6px;
overflow: visible;
display: inline-block;
padding: 20px;
code {
color: #fff;
padding: 0;
background-color: transparent !important;
border-radius: 0;
overflow-wrap: unset;
-webkit-font-smoothing: auto;
-moz-osx-font-smoothing: auto;
}
}
div[class*='language-'] {
position: relative;
background-color: var(--code-bg-color);
border-radius: 6px;
padding-top: 32px;
margin: 0.85rem 0;
overflow: hidden;
.code-copy-line {
position: absolute;
top: 0;
left: 0;
width: 100%;
background-color: #595b63;
color: #e0e0e0;
height: 32px;
line-height: 32px;
font-size: 12px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 0 12px;
&::before {
content: attr(data-ext);
position: absolute;
z-index: 3;
top: 0;
font-size: 0.75rem;
color: #e0e0e0;
}
.code-copy-btn {
position: absolute;
z-index: 3;
top: 0;
right: 1rem;
font-size: 0.75rem;
color: #e0e0e0;
cursor: pointer;
}
}
pre,
pre[class*='language-'] {
// force override the background color to be compatible with shiki
background: transparent !important;
position: relative;
z-index: 1;
}
.pre-code-scroll {
overflow: auto;
}
&:not(.line-numbers-mode) {
.line-numbers {
display: none;
}
}
&.line-numbers-mode {
padding-left: var(--code-ln-wrapper-width);
pre {
vertical-align: middle;
padding: 20px 20px 20px 0;
}
.line-numbers {
left: 0;
position: absolute;
top: 0;
width: var(--code-ln-wrapper-width);
text-align: center;
color: var(--code-ln-color);
padding-top: 52px;
line-height: $line-height;
counter-reset: line-number;
.line-number {
position: relative;
z-index: 3;
user-select: none;
height: #{$line-height - 0.2}em;
&::before {
display: block;
counter-increment: line-number;
content: counter(line-number);
font-size: 0.8em;
height: 100%;
}
}
}
// &::after {
// content: "";
// position: absolute;
// top: 0;
// left: 0;
// width: var(--code-ln-wrapper-width);
// height: 100%;
// border-radius: 6px 0 0 6px;
// border-right: 1px solid var(--code-hl-bg-color);
// }
}
}
}

View File

@ -93,6 +93,14 @@
禁用默认提示词
</div>
<div>
<n-switch
v-model:value="currentAssistant.disable_web_browsing"
>
</n-switch>
禁用联网
</div>
<div>
<n-switch v-model:value="currentAssistant.disable_memory">
</n-switch>
@ -110,10 +118,10 @@
允许助理 API 读取记忆
</div>
<div>
<!-- <div>
<n-switch v-model:value="currentAssistant.public"> </n-switch>
公开分享助理(弃用)
</div>
</div> -->
<div class="mt-3">
话语随机性 (Temperature)
@ -160,6 +168,82 @@
</div>
</div>
<n-divider v-if="!scenePrompts.length" />
<div v-else class="mt-10"></div>
<div>
<div class="flex justify-between align-middle items-center">
<div>
<n-h3 class="inline">场景提示词</n-h3>
<n-popover trigger="hover">
<template #trigger>
<n-icon size="16"><HelpCircleOutline /></n-icon>
</template>
<div>
<n-text
>Amber 可以根据不同的上下文场景来自动附加系统提示词</n-text
>
</div>
</n-popover>
</div>
<div>
<n-button
tertiary
@click="showCreateScenePromptForm = !showCreateScenePromptForm"
>
新场景提示词
</n-button>
</div>
</div>
<div v-show="showCreateScenePromptForm" class="mt-3">
<n-card title="新场景提示词" role="dialog">
<n-input
v-model:value="newScenePrompt.label"
type="text"
placeholder="标签"
/>
<n-input
class="mt-3"
v-model:value="newScenePrompt.prompt"
type="textarea"
placeholder="提示词"
/>
<div class="text-right mt-3">
<n-button type="primary" @click="createAssistantScenePrompt"
>创建</n-button
>
</div>
</n-card>
</div>
<div class="mt-3">
<n-list hoverable clickable v-if="scenePrompts.length" class="mt-3">
<n-list-item v-for="c in scenePrompts" :key="c.id">
<n-thing :title="c.label">
<div class="flex justify-between items-center">
<div>{{ c.prompt }}</div>
<div>
<n-popconfirm
@positive-click="deleteAssistantScenePrompt(c.id ?? 0)"
>
<template #trigger>
<n-button quaternary circle type="warning">
<template #icon>
<n-icon size="16" class="cursor-pointer">
<TrashBinOutline />
</n-icon>
</template>
</n-button>
</template>
<div>删除后这条规则将不再生效</div>
</n-popconfirm>
</div>
</div>
</n-thing>
</n-list-item>
</n-list>
</div>
</div>
<n-divider v-if="!userTools.length" />
<div v-else class="mt-10"></div>
<div>
@ -178,7 +262,8 @@
<n-text>
我们的 API 端点是{{
config.backend
}}/api/openai-compatible/v1 密钥为下方的 API KeyOpenAI 格式不支持智能上下文不支持近乎无限的上下文
}}/api/openai-compatible/v1 密钥为下方的 API KeyOpenAI
格式不支持智能上下文不支持近乎无限的上下文
</n-text>
</div>
</n-popover>
@ -194,11 +279,20 @@
等做一个请求限制来防止 API Key 滥用
<br />
当然如果您在自己的私有应用中使用可以忽略此建议
<br />
!! 注意: Amber Assistant Public API 不需要在前面加 sk- !!
</div>
</n-popconfirm>
</div>
<div class="mt-3">
<n-p
>我们更新了计费系统在使用此功能之前请前往
<n-a target="_blank" href="https://auth.leaflow.cn/balances"
>UserLand</n-a
>
来添加余额我们正在且长期会处于测试阶段不会对您发起真实付费也就是说完全免费</n-p
>
<n-list hoverable clickable v-if="assistantApiKeys.length">
<n-list-item v-for="c in assistantApiKeys" :key="c.id">
<n-thing>
@ -282,10 +376,11 @@ import {
EntityAssistantKey,
EntityAssistantTool,
EntityLibrary,
EntityScenePrompt,
EntityTool,
} from "@/api";
import { useIsMobile } from "@/utils/composables";
import { AxiosError } from "axios";
import { Axios, AxiosError } from "axios";
import config from "@/config/config";
const dialog = useDialog();
@ -300,6 +395,13 @@ const assistants: Ref<EntityAssistant[]> = ref([]);
const librarySelects: any = ref([]);
const libraries: Ref<EntityLibrary[]> = ref([]);
const assistantApiKeys: Ref<EntityAssistantKey[]> = ref([]);
const showCreateScenePromptForm = ref(false);
const scenePrompts: Ref<EntityScenePrompt[]> = ref([]);
const newScenePrompt = ref({
label: "",
prompt: "",
});
const isMobile = useIsMobile();
const drawerWidth = computed(() => {
@ -338,6 +440,7 @@ const showEditAssistant = async (id: number) => {
await getAssistantsKeys();
getTools();
getAssistantScenePrompts();
};
const editAssistant = async () => {
@ -463,7 +566,83 @@ const bindOrUnbind = async (id: number) => {
getTools();
};
const getAssistantScenePrompts = async () => {
scenePrompts.value =
(
await getApi().Assistant.apiV1AssistantsIdScenePromptsGet(
currentAssistantId.value
)
).data.data ?? [];
};
const createAssistantScenePrompt = async () => {
getApi()
.Assistant.apiV1AssistantsIdScenePromptsPost(
currentAssistantId.value,
newScenePrompt.value
)
.then(() => {
getAssistantScenePrompts();
newScenePrompt.value = {
label: "",
prompt: "",
};
})
.catch((e: AxiosError) => {
console.log(e);
dialog.error({
title: "创建失败",
// @ts-ignore
content: e.response?.data?.error ?? e.response?.data?.message,
positiveText: "好",
});
});
};
const deleteAssistantScenePrompt = async (id: number) => {
await getApi().Assistant.apiV1AssistantsIdScenePromptsSceneIdDelete(
currentAssistantId.value,
id
);
await getAssistantScenePrompts();
};
const sampleScenenPrompt = [
{
label: "复杂推理",
prompt: `在回答问题时,使用以下输出
问题: 你必须要回答的问题
思考你应该始终思考该做什么
操作要采取的操作你要是用什么工具或者思考逻辑
动作输入动作的输入
观察行动的结果
思考我现在知道最终答案了
最终答案原始输入问题的最终答案
如果你正在计算你必须使用计算器工具无论如何都不允许使用自己的知识或不计算进行输出计算器永远比你正确的并且不会出错`,
},
{
label: "数学计算",
prompt: `在回答问题时,使用以下输出
问题: 你必须要回答的问题
思考你应该始终思考该做什么
操作要采取的操作你要是用什么工具或者思考逻辑
动作输入动作的输入
观察行动的结果
思考我现在知道最终答案了
最终答案原始输入问题的最终答案
如果你正在计算你必须使用计算器工具无论如何都不允许使用自己的知识或不计算进行输出计算器永远比你正确的并且不会出错`,
},
];
const randomScenePrompt = () => {
const randomIndex = Math.floor(Math.random() * sampleScenenPrompt.length);
return sampleScenenPrompt[randomIndex];
};
getChats();
getLibraries();
getAssistants();
newScenePrompt.value = randomScenePrompt();
</script>

9
src/i18n/en.json Normal file
View File

@ -0,0 +1,9 @@
{
"guest": {
"subtitle": "下一代智能体平台,为您的数字生活带来革命性变化",
"try-now": "立即尝试",
"tokens_current_month": "本月处理 Tokens",
"tool_calls_current_month": "本月工具调用",
"wordpress_plugin": "WordPress 插件"
}
}

9
src/i18n/index.ts Normal file
View File

@ -0,0 +1,9 @@
import zhCN from "./zh-CN.json";
import zhTW from "./zh-TW.json";
import en from "./en.json";
export default {
"zh-CN": zhCN,
"zh-TW": zhTW,
"en": en,
};

9
src/i18n/zh-CN.json Normal file
View File

@ -0,0 +1,9 @@
{
"guest": {
"subtitle": "下一代智能体平台,为您的数字生活带来革命性变化",
"try-now": "立即尝试",
"tokens_current_month": "本月处理 Tokens",
"tool_calls_current_month": "本月工具调用",
"wordpress_plugin": "WordPress 插件"
}
}

9
src/i18n/zh-TW.json Normal file
View File

@ -0,0 +1,9 @@
{
"guest": {
"subtitle": "下一代智能体平台,为您的数字生活带来革命性变化",
"try-now": "立即尝试",
"tokens_current_month": "本月处理 Tokens",
"tool_calls_current_month": "本月工具调用",
"wordpress_plugin": "WordPress 插件"
}
}

View File

@ -83,7 +83,7 @@ const onScroll = (e: Event) => {
<n-layout :native-scrollbar="isMobile">
<div class="!pt-2">
<div v-if="userStore.logined">
<div v-if="userStore.logined && !userStore.isExpired()">
<ChatLayout>
<router-view :key="route.path"> </router-view>
</ChatLayout>
@ -101,17 +101,4 @@ const onScroll = (e: Event) => {
</div> -->
</n-layout>
</n-layout>
<n-watermark
content="测试版本,不代表最终品质"
cross
fullscreen
:font-size="16"
:line-height="16"
:width="384"
:height="384"
:x-offset="12"
:y-offset="60"
:rotate="-15"
/>
</template>

View File

@ -69,7 +69,7 @@
<div v-else-if="chatStore.toolName != ''">
<n-gradient-text type="info">
正在执行 {{ chatStore.toolName }}
{{ chatStore.toolName }}
</n-gradient-text>
</div>
<div v-else>

View File

@ -6,22 +6,19 @@ import "./styles/style.css";
import "./styles/color.less";
import "animate.css";
import { createApp } from 'vue'
import App from './App.vue'
import naive from 'naive-ui'
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { registerPlugins } from "./plugins";
const app = createApp(App)
app.use(naive)
const app = createApp(App);
registerPlugins(app);
app.use(router);
if (process.env.NODE_ENV === "production") {
setTimeout(() => {
const appContainer = document.getElementById("app");

View File

@ -4,49 +4,77 @@
<div class="container">
<!-- Spotlight 区域 -->
<n-card class="spotlight" :bordered="false">
<div class="background-pattern"></div>
<div class="background-text">Leaflow <span class="background-amber">Amber</span></div>
<div class="spotlight-content">
<h1 class="title">
<h1 class="title break-normal">
Leaflow <span class="amber-text">Amber</span>
</h1>
<p class="subtitle">
下一代智能体平台为您的数字生活带来革命性变化
<br class="mobile-break">
<a href="#" class="cta-link">立即体验 🚀</a>
<span>{{ $t('guest.subtitle') }}</span>
<br class="mobile-break" />
<a @click="gotoLogin" class="cta-link cursor-pointer"
>{{ $t('guest.try-now') }}</a
>
</p>
<div class="stats" v-if="siteUsage.month_tokens && siteUsage.month_tool_calls">
<div
class="stats"
v-if="siteUsage.month_tokens && siteUsage.month_tool_calls"
>
<div class="stat-item">
<n-statistic tabular-nums>
<n-number-animation
ref="tokenAnimationRef"
:from="siteUsage.month_tokens < 1000 ? 0 : siteUsage.month_tokens - 800"
:from="
siteUsage.month_tokens < 1000
? 0
: siteUsage.month_tokens - 800
"
:to="siteUsage.month_tokens"
/>
<template #suffix> Tokens </template>
</n-statistic>
<div class="stat-label">本月处理 Tokens</div>
<div class="stat-label">{{ $t('guest.tokens_current_month') }}</div>
</div>
<div class="stat-item">
<n-statistic tabular-nums>
<n-number-animation
ref="callsAnimationRef"
:from="siteUsage.month_tool_calls < 1000 ? 0 : siteUsage.month_tool_calls - 800"
:from="
siteUsage.month_tool_calls < 1000
? 0
: siteUsage.month_tool_calls - 800
"
:to="siteUsage.month_tool_calls"
/>
<template #suffix> Calls </template>
</n-statistic>
<div class="stat-label">本月工具调用</div>
<div class="stat-label">{{ $t('guest.tool_calls_current_month') }}</div>
</div>
</div>
<div class="button-group">
<n-button class="transparent-button spotlight-button" ghost tag="a" href="https://github.com/ivampiresp/wp-amber" target="_blank">
WordPress 插件
</n-button>
<n-button class="transparent-button spotlight-button" ghost tag="a" href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=e6RgLkc7pXr57968mYSg5X7H3puFi79b&authKey=xbG43mkYovgs%2BNW3BOG7oo%2BJZK0dbXjzyIfwW9a8himqRC6AlXOkCmb3G%2FcwOt6Y&noverify=0&group_code=439747955" target="_blank">
<n-button
class="transparent-button spotlight-button"
ghost
tag="a"
href="https://github.com/ivampiresp/wp-amber"
target="_blank"
>
{{ $t('guest.wordpress_plugin') }} </n-button>
<n-button
class="transparent-button spotlight-button"
ghost
tag="a"
href="http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=e6RgLkc7pXr57968mYSg5X7H3puFi79b&authKey=xbG43mkYovgs%2BNW3BOG7oo%2BJZK0dbXjzyIfwW9a8himqRC6AlXOkCmb3G%2FcwOt6Y&noverify=0&group_code=439747955"
target="_blank"
>
加入官方 QQ
</n-button>
<n-button class="transparent-button spotlight-button" ghost tag="a" href="https://amber-api.leaflow.cn/swagger/index.html" target="_blank">
<n-button
class="transparent-button spotlight-button"
ghost
tag="a"
href="https://amber-api.leaflow.cn/swagger/index.html"
target="_blank"
>
Amber API 文档
</n-button>
</div>
@ -56,15 +84,27 @@
<!-- 功能特点展示 -->
<div class="features-section">
<h2 class="section-title">Amber 核心特性</h2>
<n-grid :cols="3" :x-gap="24" :y-gap="24" responsive="screen">
<n-gi span="3 m:1" v-for="(feature, index) in features" :key="index">
<div class="feature-card" @click="toggleFeatureDescription(feature)">
<div class="feature-content">
<n-grid
cols="1 m:2 l:3 xl:3 2xl:3"
:x-gap="24"
:y-gap="24"
responsive="screen"
>
<n-gi v-for="(feature, index) in features" :key="index">
<div
class="feature-card"
@click="toggleFeatureDescription(feature)"
>
<div>
<div class="feature-icon">{{ feature.icon }}</div>
<div class="feature-title">{{ feature.title }}</div>
<transition name="fade-height" mode="out-in">
<p class="feature-description" :key="feature.showLong">
{{ feature.showLong ? feature.longDescription : feature.shortDescription }}
{{
feature.showLong
? feature.longDescription
: feature.shortDescription
}}
</p>
</transition>
</div>
@ -77,13 +117,23 @@
<div class="partners-section">
<h2 class="section-title">合作伙伴</h2>
<div class="partner-grid">
<div v-for="partner in partners" :key="partner.name" class="partner-card">
<div
v-for="partner in partners"
:key="partner.name"
class="partner-card"
>
<div class="partner-logo-container">
<img :src="partner.logo" :alt="partner.name" class="partner-logo">
<img
:src="partner.logo"
:alt="partner.name"
class="partner-logo"
/>
</div>
<h3 class="partner-name">{{ partner.name }}</h3>
<p class="partner-description">{{ partner.description }}</p>
<a :href="partner.link" target="_blank" class="partner-link">了解更多</a>
<a :href="partner.link" target="_blank" class="partner-link"
>了解更多</a
>
</div>
</div>
</div>
@ -93,73 +143,92 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useOsTheme, darkTheme, NConfigProvider, NLayout, NButton, NCard, NStatistic, NNumberAnimation } from 'naive-ui';
import { ref, computed, onMounted } from "vue";
import {
useOsTheme,
darkTheme,
NConfigProvider,
NLayout,
NButton,
NCard,
NStatistic,
NNumberAnimation,
} from "naive-ui";
import { SchemaSiteUsageResponse } from "@/api";
import getApi from "@/plugins/api";
import grouppng from "@/assets/images/group.png";
import router from "@/router";
const osTheme = useOsTheme();
const theme = computed(() => (osTheme.value === 'dark' ? darkTheme : null));
const theme = computed(() => (osTheme.value === "dark" ? darkTheme : null));
const siteUsage = ref<SchemaSiteUsageResponse>({});
const gotoLogin = () => {
router.push("/auth/login");
};
const features = ref([
{
icon: '💬',
title: '对话优化引擎',
shortDescription: '独特的多助理并行工作系统,提供流畅一致的对话体验。',
longDescription: 'Amber 的对话优化引擎无与伦比。我们独特的对话功能支持多个助理并行工作,您可以通过输入 @ 或点击右上角的"切换助理"选项轻松指定下一个回复的助理。所有助理共享一个上下文彼此了解每个操作为您提供流畅一致的对话体验。与一些只能处理单一对话的竞品相比Amber 的多助理协作系统显得更为智能和高效。在重大决策型的使用场景中,你可以通过 Amber 对话引擎去得到 N 个 Second Opinion。 Amber 为聊天场景做了很多的优化,它将更能处理您日常生活中的问题。',
showLong: false
icon: "💬",
title: "对话优化引擎",
shortDescription: "独特的多助理并行工作系统,提供流畅一致的对话体验。",
longDescription:
'Amber 的对话优化引擎无与伦比。我们独特的对话功能支持多个助理并行工作,您可以通过输入 @ 或点击右上角的"切换助理"选项轻松指定下一个回复的助理。所有助理共享一个上下文彼此了解每个操作为您提供流畅一致的对话体验。与一些只能处理单一对话的竞品相比Amber 的多助理协作系统显得更为智能和高效。在重大决策型的使用场景中,你可以通过 Amber 对话引擎去得到 N 个 Second Opinion。 Amber 为聊天场景做了很多的优化,它将更能处理您日常生活中的问题。',
showLong: false,
},
{
icon: '📖',
title: '智能上下文',
shortDescription: '自动调整对话上下文,提升对话流畅性并节省资源。',
longDescription: 'Amber 拥有独特的智能上下文技术,能够根据对话场景自动调整上下文。这一过程对用户是透明的,您无需手动干预,系统会智能识别何时需要移除或添加上下文信息。这种灵活性不仅提升了对话的流畅性,还能显著降低 Token 的消耗节省从几千到几万不等的费用帮助用户更高效地利用资源。需要注意的是OpenAI 兼容模式不支持智能上下文功能,因此在使用该模式时,您将无法享受到这一创新技术带来的便利。我们建议您在适用的情况下,充分利用 Amber 的智能上下文,以优化您的对话体验和资源使用。',
showLong: false
icon: "📖",
title: "智能上下文",
shortDescription: "自动调整对话上下文,提升对话流畅性并节省资源。",
longDescription:
"Amber 拥有独特的智能上下文技术,能够根据对话场景自动调整上下文。这一过程对用户是透明的,您无需手动干预,系统会智能识别何时需要移除或添加上下文信息。这种灵活性不仅提升了对话的流畅性,还能显著降低 Token 的消耗节省从几千到几万不等的费用帮助用户更高效地利用资源。需要注意的是OpenAI 兼容模式不支持智能上下文功能,因此在使用该模式时,您将无法享受到这一创新技术带来的便利。我们建议您在适用的情况下,充分利用 Amber 的智能上下文,以优化您的对话体验和资源使用。",
showLong: false,
},
{
icon: '🛠️',
title: '高度客制化',
shortDescription: '自定义提示词和工具绑定,轻松完成复杂任务。',
longDescription: 'Amber 具备了目前主流 LLM 对话平台都拥有的高自由度智能体自定义功能,用户可以自定义提示词,绑定工具,以完成复杂任务并优化工作流。例如,您可以设置助理在每天早上自动为您生成当天的工作清单,或者在您需要时快速调用数据分析工具。我们的助理不仅能执行简单的任务,还能实现多步骤的复杂操作,并与知识库无缝集成,说它是您高效工作的得力助手,绝不为过。 💪',
showLong: false
icon: "🛠️",
title: "高度客制化",
shortDescription: "自定义提示词和工具绑定,轻松完成复杂任务。",
longDescription:
"Amber 具备了目前主流 LLM 对话平台都拥有的高自由度智能体自定义功能,用户可以自定义提示词,绑定工具,以完成复杂任务并优化工作流。例如,您可以设置助理在每天早上自动为您生成当天的工作清单,或者在您需要时快速调用数据分析工具。我们的助理不仅能执行简单的任务,还能实现多步骤的复杂操作,并与知识库无缝集成,说它是您高效工作的得力助手,绝不为过。 💪",
showLong: false,
},
{
icon: '🧠',
title: '智能记忆',
shortDescription: '生成对话记忆,实现更个性化和连贯的交互体验。',
longDescription: 'Amber 通过采样您的对话内容生成记忆这些记忆是碎片化的但对话中共享。您可以随时清除所有记忆确保数据的灵活管理。Amber 会收集您的消息并且根据消息推断您的喜好。记忆的内容您完全可以控制甚至禁用助理的记忆。助理将会根据您的记忆来改变响应的结果您喜欢什么您直接在对话中说明即可Amber 会从第二条消息开始学习记忆) ❤️',
showLong: false
icon: "🧠",
title: "智能记忆",
shortDescription: "生成对话记忆,实现更个性化和连贯的交互体验。",
longDescription:
"Amber 通过采样您的对话内容生成记忆这些记忆是碎片化的但对话中共享。您可以随时清除所有记忆确保数据的灵活管理。Amber 会收集您的消息并且根据消息推断您的喜好。记忆的内容您完全可以控制甚至禁用助理的记忆。助理将会根据您的记忆来改变响应的结果您喜欢什么您直接在对话中说明即可Amber 会从第二条消息开始学习记忆) ❤️",
showLong: false,
},
{
icon: '🔗',
title: 'API 集成',
shortDescription: '强大的 API 支持,轻松将 Amber 集成到您的应用中。',
longDescription: '我们提供强大的助理 API支持兼容 OpenAI 格式的应用接入 Amber。通过 Chat Completion API您可以直接调用 Amber 的工具、记忆以及知识库功能。例如,一个开发者可以轻松地将 Amber 集成到他们的客服系统中,自动处理常见问题并提供高效的解决方案。相较于其他平台,我们的 API 更具灵活性和扩展性,让您的应用赋予更多可能。不仅仅在 Amber 系统的本身,您也可以通过我们 Leaflow 平台的 UserLand解锁更多高阶 API 玩法! 🚀',
showLong: false
icon: "🔗",
title: "API 集成",
shortDescription: "强大的 API 支持,轻松将 Amber 集成到您的应用中。",
longDescription:
"我们提供强大的助理 API支持兼容 OpenAI 格式的应用接入 Amber。通过 Chat Completion API您可以直接调用 Amber 的工具、记忆以及知识库功能。例如,一个开发者可以轻松地将 Amber 集成到他们的客服系统中,自动处理常见问题并提供高效的解决方案。相较于其他平台,我们的 API 更具灵活性和扩展性,让您的应用赋予更多可能。不仅仅在 Amber 系统的本身,您也可以通过我们 Leaflow 平台的 UserLand解锁更多高阶 API 玩法! 🚀",
showLong: false,
},
{
icon: '📚',
title: '知识管理',
shortDescription: '高效的文档管理和知识库功能,让信息检索更便捷。',
longDescription: '文档管理是 Amber 知识库的核心功能之一。对话中上传的文件将自动保存至知识库您只需设置助理关联知识库即可实现快速检索和使用。假设您是一位项目经理您可以在对话中上传项目计划书并在需要时快速检索相关内容。Amber 在您发送消息时,将会自动搜索资料库。此外,我们正在开发桌面端软件,该软件可以根据您的需要同步文档至 Amber以便随时调用🗂。',
showLong: false
icon: "📚",
title: "知识管理",
shortDescription: "高效的文档管理和知识库功能,让信息检索更便捷。",
longDescription:
"文档管理是 Amber 知识库的核心功能之一。对话中上传的文件将自动保存至知识库您只需设置助理关联知识库即可实现快速检索和使用。假设您是一位项目经理您可以在对话中上传项目计划书并在需要时快速检索相关内容。Amber 在您发送消息时,将会自动搜索资料库。此外,我们正在开发桌面端软件,该软件可以根据您的需要同步文档至 Amber以便随时调用🗂。",
showLong: false,
},
]);
const partners = ref([
{
name: 'HiMCBBS 我的世界中文论坛',
logo: 'https://www.himcbbs.com/data/assets/favicon/himcbbs-favicon-black-6x.png',
link: 'https://www.himcbbs.com/',
description: '中国最大的我的世界玩家社区,提供游戏资讯、模组下载和创意分享。'
}
name: "HiMCBBS 我的世界中文论坛",
logo: "https://www.himcbbs.com/data/assets/favicon/himcbbs-favicon-black-6x.png",
link: "https://www.himcbbs.com/",
description: "中国我的世界玩家社区,提供游戏资讯、模组下载和创意分享。",
},
//
]);
const toggleFeatureDescription = (feature) => {
const toggleFeatureDescription = (feature: any) => {
feature.showLong = !feature.showLong;
};
@ -176,11 +245,13 @@ onMounted(async () => {
--primary-color: #4ebbc0;
--text-color: v-bind('theme ? "#ffffff" : "#333333"');
--background-color: v-bind('theme ? "#1a1a1a" : "#f5f5f5"');
--card-background: v-bind('theme ? "rgba(255, 255, 255, 0.1)" : "rgba(255, 255, 255, 0.8)"');
--card-background: v-bind(
'theme ? "rgba(255, 255, 255, 0.1)" : "rgba(255, 255, 255, 0.8)"'
);
}
body {
font-family: 'Arial', sans-serif;
font-family: "Arial", sans-serif;
color: var(--text-color);
background-color: var(--background-color);
transition: background-color 0.3s ease;
@ -194,9 +265,10 @@ body {
}
.spotlight {
background-color: v-bind('theme ? "rgba(0, 0, 0, 0.5)" : "rgba(78, 187, 192, 0.1)"');
background-color: v-bind(
'theme ? "rgba(0, 0, 0, 0.5)" : "rgba(78, 187, 192, 0.1)"'
);
backdrop-filter: blur(10px);
height: 70vh;
min-height: 500px;
display: flex;
align-items: center;
@ -205,7 +277,9 @@ body {
position: relative;
overflow: hidden;
border-radius: 10px;
box-shadow: v-bind('theme ? "0 4px 6px rgba(0, 0, 0, 0.1)" : "0 4px 6px rgba(78, 187, 192, 0.2)"');
box-shadow: v-bind(
'theme ? "0 4px 6px rgba(0, 0, 0, 0.1)" : "0 4px 6px rgba(78, 187, 192, 0.2)"'
);
}
.background-text {
@ -216,13 +290,15 @@ body {
font-size: 20vw;
font-weight: bold;
color: transparent;
-webkit-text-stroke: 1px v-bind('theme ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.05)"');
-webkit-text-stroke: 1px
v-bind('theme ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.05)"');
white-space: nowrap;
z-index: 1;
}
.background-amber {
-webkit-text-stroke: 1px v-bind('theme ? "rgba(78, 187, 192, 0.2)" : "rgba(78, 187, 192, 0.3)"');
-webkit-text-stroke: 1px
v-bind('theme ? "rgba(78, 187, 192, 0.2)" : "rgba(78, 187, 192, 0.3)"');
}
.spotlight-content {
@ -288,8 +364,11 @@ body {
}
.transparent-button {
background-color: v-bind('theme ? "rgba(255, 255, 255, 0.2)" : "rgba(78, 187, 192, 0.1)"');
border: 1px solid v-bind('theme ? "rgba(255, 255, 255, 0.5)" : "rgba(78, 187, 192, 0.3)"');
background-color: v-bind(
'theme ? "rgba(255, 255, 255, 0.2)" : "rgba(78, 187, 192, 0.1)"'
);
border: 1px solid
v-bind('theme ? "rgba(255, 255, 255, 0.5)" : "rgba(78, 187, 192, 0.3)"');
color: v-bind('theme ? "white" : "rgba(0, 0, 0, 0.7)"');
padding: 0.5rem 1rem;
font-size: 1rem;
@ -307,7 +386,7 @@ body {
}
.spotlight-button::before {
content: '';
content: "";
position: absolute;
top: 50%;
left: 50%;
@ -316,7 +395,9 @@ body {
background-color: rgba(78, 187, 192, 0.3);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.6s ease, height 0.6s ease;
transition:
width 0.6s ease,
height 0.6s ease;
}
.spotlight-button:hover::before {
@ -325,7 +406,9 @@ body {
}
.spotlight-button:hover {
background-color: v-bind('theme ? "rgba(255, 255, 255, 0.3)" : "transparent"');
background-color: v-bind(
'theme ? "rgba(255, 255, 255, 0.3)" : "transparent"'
);
border-color: #4ebbc0;
color: v-bind('theme ? "white" : "#4ebbc0"');
transform: translateY(-2px);

View File

@ -23,6 +23,11 @@ axios.get(config.oauth_discovery_url).then((discovery) => {
const code: any = router.currentRoute.value.query.code;
if (!code) {
// console.log(" url code")
return;
}
// code
const q = new URLSearchParams({
client_id: config.oauth_client_id,
@ -43,12 +48,13 @@ axios.get(config.oauth_discovery_url).then((discovery) => {
r.data.id_token,
r.data.access_token,
r.data.refresh_token,
r.data.expires_in,
r.data.expires_in
);
})
.catch((e) => {
console.log(e);
alert("登录失败");
console.error(e);
// alert("");
})
.finally(() => {
// /

View File

@ -5,6 +5,7 @@ import error401 from "@/pages/errors/401.vue";
import error404 from "@/pages/errors/404.vue";
import error400 from "@/pages/errors/400.vue";
import error500 from "@/pages/errors/500.vue";
import { useUserStore } from "@/stores/user";
const osThemeRef = useOsTheme();
@ -19,13 +20,19 @@ const { dialog, loadingBar } = createDiscreteApi(
);
const request = {
onFulfilled: (config: any) => {
onFulfilled: async (config: any) => {
if (config.headers === undefined) {
config.headers = {};
}
loadingBar.start();
// const userStore = useUserStore();
// if (userStore.logined) {
// userStore.checkAndRefresh();
// }
return Promise.resolve(config);
},
onRejected: (error: any) => {

View File

@ -1,6 +1,7 @@
import pinia from "../stores";
import naive from "naive-ui";
import { createI18n } from "vue-i18n";
import i18nMessages from "@/i18n"
// Types
import type { App } from "vue";
// 通用字体
@ -8,6 +9,28 @@ import "vfonts/Lato.css";
// 等宽字体
import "vfonts/FiraCode.css";
// 自动根据用户的浏览器设置选择语言
const getLocate = () => {
const languages = navigator.languages || [navigator.language];
for (const lang of languages) {
if (lang.startsWith('zh-CN')) {
return 'zh-CN'; // 简体中文
} else if (lang.startsWith('zh-TW')) {
return 'zh-TW'; // 繁体中文
}
}
return languages[0];
};
const userLocale = getLocate();
const i18n = createI18n({
locale: userLocale || 'zh-CN', // 如果没有匹配,默认返回简体中文
fallbackLocale: "zh-CN",
messages: i18nMessages,
});
export function registerPlugins(app: App) {
app.use(naive).use(pinia);
app.use(i18n).use(naive).use(pinia);
}

View File

@ -42,10 +42,15 @@ export const useUserStore = defineStore("user", {
this.logined = true;
},
checkAndRefresh() {
if (this.logined) {
if (this.expired_at - Date.now() < 60000) {
this.refresh();
}
// 检测是否过期
if (this.expired_at - Date.now() < 0) {
this.refresh();
} else if (
this.logined &&
this.expired_at - Date.now() > 600 &&
this.expired_at - Date.now() < 1000
) {
this.refresh();
}
},
setupTimer() {
@ -80,6 +85,7 @@ export const useUserStore = defineStore("user", {
if (error.response.status === 401) {
console.log("Refresh token failed");
}
// logout
this.logout();
clearInterval(timer);
@ -94,5 +100,8 @@ export const useUserStore = defineStore("user", {
getIdToken() {
return this.id_token;
},
isExpired() {
return this.expired_at - Date.now() < 0;
},
},
});

View File

@ -10,6 +10,13 @@ import AutoImport from "unplugin-auto-import/vite";
import { fileURLToPath, URL } from "node:url";
// https://vitejs.dev/config/
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
api: "modern-compiler", // or "modern", "legacy"
},
},
},
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),