diff --git a/.dockerignore b/.dockerignore index fe1152bdb8442f4d14f9b9533e63fe0c2680bcee..9a1aea6ab95903945c805b790b7b986b4296571d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -27,4 +27,7 @@ README.md !.git/HEAD !.git/config !.git/packed-refs -!.git/refs/heads/** \ No newline at end of file +!.git/refs/heads/** +**/disk +**/.idea +**/postgresql \ No newline at end of file diff --git a/FastWiki.sln b/FastWiki.sln index 5fce12435c3534733b9ac2f4a751d5b20bd63428..c29b506a6393f22ed0dc2edf848705a45ae9d9d1 100644 --- a/FastWiki.sln +++ b/FastWiki.sln @@ -14,6 +14,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution items", "solution Directory.Build.props = Directory.Build.props docker-compose.yml = docker-compose.yml README.md = README.md + docker-compose-blazor.yml = docker-compose-blazor.yml + .dockerignore = .dockerignore + .gitignore = .gitignore EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastWiki.Web.Server", "src\Web\FastWiki.Web.Server\FastWiki.Web.Server.csproj", "{131196C0-8908-4A85-BD7A-9758A1BDCC50}" diff --git a/docker-compose-blazor.yml b/docker-compose-blazor.yml new file mode 100644 index 0000000000000000000000000000000000000000..55209a087fc7b95904a7fe60fde13507050db518 --- /dev/null +++ b/docker-compose-blazor.yml @@ -0,0 +1,45 @@ +version: '3.8' # 可以根据需要使用不同的版本 +services: + fast-wiki-service: + image: registry.cn-shenzhen.aliyuncs.com/fast-wiki/fast-wiki-service + container_name: fast-wiki-service + user: root + restart: always + ports: + - "8080:8080" + build: + context: . + dockerfile: ./src/Service/FastWiki.Service/Dockerfile + volumes: + - ./wwwroot:/app/wwwroot/ + environment: + - OPENAI_CHAT_ENDPOINT=https://api.openai.com + - OPENAI_CHAT_EMBEDDING_ENDPOINT=https://api.openai.com + - OPENAI_CHAT_TOKEN={您的TokenKey} + - OPENAI_CHAT_MODEL=gpt-3.5-turbo + - OPENAI_EMBEDDING_MODEL=text-embedding-3-small + - ASPNETCORE_ENVIRONMENT=Development + + postgres: # 当前compose服务名 + image: registry.cn-shenzhen.aliyuncs.com/fast-wiki/pgvector:v0.5.0 # 拉取的数据库镜像 + container_name: postgres # 容器运行的容器名称 + restart: always # 开机自启动 + environment: # 环境变量 + POSTGRES_USER: token # 默认账号 + POSTGRES_PASSWORD: dd666666 # 默认密码 + POSTGRES_DB: wiki # 默认数据库 + TZ: Asia/Shanghai # 数据库时区 + volumes: + - ./postgresql:/var/lib/postgresql/data # 将PostgreSql数据持久化 + + fast-wiki-server: + image: registry.cn-shenzhen.aliyuncs.com/fast-wiki/fast-wiki-server + container_name: fast-wiki-server + restart: always + ports: + - "2180:8080" + build: + context: . + dockerfile: ./src/Web/FastWiki.Web.Server/Dockerfile + environment: + - FAST_WIKI_SERVICE=http://知识库api的ip:8080 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 55209a087fc7b95904a7fe60fde13507050db518..22e704b3083237a8fc815a963e2ae18e403e85ad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,8 +10,10 @@ services: build: context: . dockerfile: ./src/Service/FastWiki.Service/Dockerfile + depends_on: + - postgres volumes: - - ./wwwroot:/app/wwwroot/ + - ./wwwroot/uploads:/app/wwwroot/uploads environment: - OPENAI_CHAT_ENDPOINT=https://api.openai.com - OPENAI_CHAT_EMBEDDING_ENDPOINT=https://api.openai.com @@ -30,16 +32,4 @@ services: POSTGRES_DB: wiki # 默认数据库 TZ: Asia/Shanghai # 数据库时区 volumes: - - ./postgresql:/var/lib/postgresql/data # 将PostgreSql数据持久化 - - fast-wiki-server: - image: registry.cn-shenzhen.aliyuncs.com/fast-wiki/fast-wiki-server - container_name: fast-wiki-server - restart: always - ports: - - "2180:8080" - build: - context: . - dockerfile: ./src/Web/FastWiki.Web.Server/Dockerfile - environment: - - FAST_WIKI_SERVICE=http://知识库api的ip:8080 \ No newline at end of file + - ./postgresql:/var/lib/postgresql/data # 将PostgreSql数据持久化 \ No newline at end of file diff --git a/src/ApiGateway/FastWiki.ApiGateway.Caller/Service/ChatApplicationService.cs b/src/ApiGateway/FastWiki.ApiGateway.Caller/Service/ChatApplicationService.cs index e910bce7921536e9a20d22e66204f6ba230f50bf..fd4e4438cc9c9a303d232c9019c9548587f178b9 100644 --- a/src/ApiGateway/FastWiki.ApiGateway.Caller/Service/ChatApplicationService.cs +++ b/src/ApiGateway/FastWiki.ApiGateway.Caller/Service/ChatApplicationService.cs @@ -5,7 +5,10 @@ using System.Net.Http.Json; namespace FastWiki.ApiGateway.Caller.Service; -public sealed class ChatApplicationService(ICaller caller, IHttpClientFactory httpClientFactory, IUserService userService) +public sealed class ChatApplicationService( + ICaller caller, + IHttpClientFactory httpClientFactory, + IUserService userService) : ServiceBase(caller, httpClientFactory, userService), IChatApplicationService { protected override string BaseUrl { get; set; } = "ChatApplications"; @@ -52,7 +55,8 @@ public sealed class ChatApplicationService(ICaller caller, IHttpClientFactory ht public async Task CreateChatDialogAsync(CreateChatDialogInput input) { - await PostAsync(nameof(CreateChatDialogAsync), input).ConfigureAwait(false); ; + await PostAsync(nameof(CreateChatDialogAsync), input).ConfigureAwait(false); + ; } public async Task> GetChatDialogAsync(string chatId, bool all) @@ -60,10 +64,10 @@ public sealed class ChatApplicationService(ICaller caller, IHttpClientFactory ht return await GetAsync>(nameof(GetChatDialogAsync), new Dictionary() { { - "applicationId",chatId + "applicationId", chatId }, { - "all",all.ToString() + "all", all.ToString() }, }); } @@ -73,7 +77,7 @@ public sealed class ChatApplicationService(ICaller caller, IHttpClientFactory ht return await GetAsync>(nameof(GetChatShareDialogAsync), new Dictionary() { { - "chatId",chatId + "chatId", chatId } }); } @@ -114,7 +118,8 @@ public sealed class ChatApplicationService(ICaller caller, IHttpClientFactory ht public async Task CreateChatDialogHistoryAsync(CreateChatDialogHistoryInput input) { - await PostAsync(nameof(CreateChatDialogHistoryAsync), input).ConfigureAwait(false); ; + await PostAsync(nameof(CreateChatDialogHistoryAsync), input).ConfigureAwait(false); + ; } public async Task> GetChatDialogHistoryAsync(string chatDialogId, int page, @@ -133,17 +138,20 @@ public sealed class ChatApplicationService(ICaller caller, IHttpClientFactory ht nameof(pageSize), pageSize.ToString() } - }).ConfigureAwait(false); ; + }).ConfigureAwait(false); + ; } public async Task RemoveDialogHistoryAsync(string id) { - await DeleteAsync(nameof(RemoveDialogHistoryAsync) + "/" + id).ConfigureAwait(false); ; + await DeleteAsync(nameof(RemoveDialogHistoryAsync) + "/" + id).ConfigureAwait(false); + ; } public async Task CreateShareAsync(CreateChatShareInput input) { - await PostAsync(nameof(CreateShareAsync), input).ConfigureAwait(false); ; + await PostAsync(nameof(CreateShareAsync), input).ConfigureAwait(false); + ; } public async Task> GetChatShareListAsync(string chatApplicationId, int page, @@ -161,12 +169,14 @@ public sealed class ChatApplicationService(ICaller caller, IHttpClientFactory ht { "pageSize", pageSize.ToString() } - }).ConfigureAwait(false); ; + }).ConfigureAwait(false); + ; } public async Task RemoveDialogAsync(string id) { - await DeleteAsync(nameof(RemoveDialogAsync) + "/" + id).ConfigureAwait(false); ; + await DeleteAsync(nameof(RemoveDialogAsync) + "/" + id).ConfigureAwait(false); + ; } public async Task UpdateDialogAsync(ChatDialogDto input) @@ -176,7 +186,8 @@ public sealed class ChatApplicationService(ICaller caller, IHttpClientFactory ht public async Task RemoveShareDialogAsync(string chatId, string id) { - await DeleteAsync(nameof(RemoveDialogAsync) + "/" + id + "?chatId=" + chatId).ConfigureAwait(false); ; + await DeleteAsync(nameof(RemoveDialogAsync) + "/" + id + "?chatId=" + chatId).ConfigureAwait(false); + ; } public async Task UpdateShareDialogAsync(ChatDialogDto input) @@ -184,24 +195,27 @@ public sealed class ChatApplicationService(ICaller caller, IHttpClientFactory ht await PutAsync(nameof(UpdateDialogAsync), input).ConfigureAwait(false); } - public async Task> GetSessionLogDialogAsync(string chatApplicationId, int page, int pageSize) + public async Task> GetSessionLogDialogAsync(string chatApplicationId, int page, + int pageSize) { return await GetAsync>(nameof(GetSessionLogDialogAsync), new Dictionary() { { - - "chatApplicationId",chatApplicationId + "chatApplicationId", chatApplicationId }, { - - "page",page.ToString() + "page", page.ToString() }, { - - "pageSize",pageSize.ToString() + "pageSize", pageSize.ToString() } }); } + + public async Task PutChatHistoryAsync(PutChatHistoryInput input) + { + await PutAsync(nameof(PutChatHistoryAsync), input).ConfigureAwait(false); + } } \ No newline at end of file diff --git a/src/Contracts/FastWiki.Service.Contracts/ChatApplication/Dto/ChatDialogHistoryDto.cs b/src/Contracts/FastWiki.Service.Contracts/ChatApplication/Dto/ChatDialogHistoryDto.cs index 13841f4f58bf8c47b9714173ed13b1ef3fa30f3f..d627f38d59bc3a9449c29f7dd8b6d9769ee02532 100644 --- a/src/Contracts/FastWiki.Service.Contracts/ChatApplication/Dto/ChatDialogHistoryDto.cs +++ b/src/Contracts/FastWiki.Service.Contracts/ChatApplication/Dto/ChatDialogHistoryDto.cs @@ -35,4 +35,6 @@ public sealed class ChatDialogHistoryDto public List SourceFile { get; set; } = new(); public DateTime CreationTime { get; set; } + + public long CreateAt => (long)(CreationTime.ToUniversalTime() - new DateTime(1970, 1, 1)).TotalMilliseconds; } \ No newline at end of file diff --git a/src/Contracts/FastWiki.Service.Contracts/ChatApplication/Dto/PutChatHistoryInput.cs b/src/Contracts/FastWiki.Service.Contracts/ChatApplication/Dto/PutChatHistoryInput.cs new file mode 100644 index 0000000000000000000000000000000000000000..55299d30a6bba3e6fcbf8ffc5afed4d73a39f019 --- /dev/null +++ b/src/Contracts/FastWiki.Service.Contracts/ChatApplication/Dto/PutChatHistoryInput.cs @@ -0,0 +1,11 @@ +namespace FastWiki.Service.Contracts.ChatApplication.Dto; + +public sealed class PutChatHistoryInput +{ + public string Id { get; set; } + + public string Content { get; set; } + + public string? ChatShareId { get; set; } + +} \ No newline at end of file diff --git a/src/Contracts/FastWiki.Service.Contracts/ChatApplication/IChatApplicationService.cs b/src/Contracts/FastWiki.Service.Contracts/ChatApplication/IChatApplicationService.cs index 30c212d25d5027edd1d939ba71e1e6202e69f14e..99eddd55c2d7eb90fc18338bcecf28c371986acd 100644 --- a/src/Contracts/FastWiki.Service.Contracts/ChatApplication/IChatApplicationService.cs +++ b/src/Contracts/FastWiki.Service.Contracts/ChatApplication/IChatApplicationService.cs @@ -160,4 +160,10 @@ public interface IChatApplicationService /// /// Task> GetSessionLogDialogAsync(string chatApplicationId, int page, int pageSize); + + /// + /// 修改对话记录内容 + /// + /// + Task PutChatHistoryAsync(PutChatHistoryInput input); } \ No newline at end of file diff --git a/src/Contracts/FastWiki.Service.Contracts/Wikis/Dto/WikiDetailVectorQuantityDto.cs b/src/Contracts/FastWiki.Service.Contracts/Wikis/Dto/WikiDetailVectorQuantityDto.cs index da1600b3fe49b9d6d1b9776c8234915fde96a9d3..1f46ef57b707266a299da1d6b4b8652341d5a699 100644 --- a/src/Contracts/FastWiki.Service.Contracts/Wikis/Dto/WikiDetailVectorQuantityDto.cs +++ b/src/Contracts/FastWiki.Service.Contracts/Wikis/Dto/WikiDetailVectorQuantityDto.cs @@ -2,6 +2,11 @@ public sealed class WikiDetailVectorQuantityDto { + /// + /// 当前索引 + /// + public int Index { get; set; } + public string Id { get; set; } /// diff --git a/src/Service/FastWiki.Service/Application/ChatApplications/ChatApplicationCommandHandler.cs b/src/Service/FastWiki.Service/Application/ChatApplications/ChatApplicationCommandHandler.cs index ffac551359111fe42dbe6f0dd5e88a0557c3f75d..6510a41e41a0b9ca261c871c627bcd044aed299c 100644 --- a/src/Service/FastWiki.Service/Application/ChatApplications/ChatApplicationCommandHandler.cs +++ b/src/Service/FastWiki.Service/Application/ChatApplications/ChatApplicationCommandHandler.cs @@ -92,7 +92,7 @@ public class ChatApplicationCommandHandler( [EventHandler] public async Task RemoveShareDialogAsync(RemoveShareDialogCommand command) { - await chatApplicationRepository.RemoveShareDialogAsync(command.ChatId,command.Id); + await chatApplicationRepository.RemoveShareDialogAsync(command.ChatId, command.Id); } [EventHandler] @@ -100,4 +100,11 @@ public class ChatApplicationCommandHandler( { await chatApplicationRepository.UpdateShareDialogAsync(mapper.Map(command.Input)); } + + [EventHandler] + public async Task UpdateChatShareAsync(PutChatHistoryCommand command) + { + await chatApplicationRepository.PutChatHistoryAsync(command.Input.Id, command.Input.Content, + command.Input.ChatShareId); + } } \ No newline at end of file diff --git a/src/Service/FastWiki.Service/Application/ChatApplications/Commands/PutChatHistoryCommand.cs b/src/Service/FastWiki.Service/Application/ChatApplications/Commands/PutChatHistoryCommand.cs new file mode 100644 index 0000000000000000000000000000000000000000..4e22a858de742c462826b5fb9dee2d69eb15de00 --- /dev/null +++ b/src/Service/FastWiki.Service/Application/ChatApplications/Commands/PutChatHistoryCommand.cs @@ -0,0 +1,7 @@ +namespace FastWiki.Service.Application.ChatApplications.Commands; + +/// +/// ޸ļ¼ +/// +/// +public record PutChatHistoryCommand(PutChatHistoryInput Input):Command; \ No newline at end of file diff --git a/src/Service/FastWiki.Service/Application/Wikis/WikiQueryHandler.cs b/src/Service/FastWiki.Service/Application/Wikis/WikiQueryHandler.cs index 45d39d38e58b95c591a5f92431e8d32f37dbafb0..c3f86e327c1867ed3f802c7a80a898f2db01f440 100644 --- a/src/Service/FastWiki.Service/Application/Wikis/WikiQueryHandler.cs +++ b/src/Service/FastWiki.Service/Application/Wikis/WikiQueryHandler.cs @@ -95,6 +95,7 @@ public sealed class WikiQueryHandler( Content = item.Payload["text"].ToString() ?? string.Empty, FileId = item.Tags.FirstOrDefault(x=>x.Key=="fileId").Value?.FirstOrDefault() ?? string.Empty, Id = item.Id, + Index = size, WikiDetailId = item.Tags["wikiDetailId"].FirstOrDefault() ?? string.Empty, Document_Id = item.Tags["__document_id"].FirstOrDefault() ?? string.Empty }); diff --git a/src/Service/FastWiki.Service/DataAccess/Repositories/ChatApplications/ChatApplicationReoisutory.cs b/src/Service/FastWiki.Service/DataAccess/Repositories/ChatApplications/ChatApplicationReoisutory.cs index ef01fdf68e85a1a6963e9d3b43d0c6208fc723c8..70352c5b7a114d400c3e07b5f2b5508014bf9f3f 100644 --- a/src/Service/FastWiki.Service/DataAccess/Repositories/ChatApplications/ChatApplicationReoisutory.cs +++ b/src/Service/FastWiki.Service/DataAccess/Repositories/ChatApplications/ChatApplicationReoisutory.cs @@ -31,6 +31,8 @@ public sealed class ChatApplicationReoisutory(WikiDbContext context, IUnitOfWork { await Context.ChatDialogs.Where(x => x.Id == id).ExecuteDeleteAsync(); await Context.ChatDialogHistorys.Where(x => x.ChatDialogId == id).ExecuteDeleteAsync(); + + await Context.SaveChangesAsync(); } public async Task> GetChatDialogListAsync(string applicationId, bool all) @@ -169,6 +171,29 @@ public sealed class ChatApplicationReoisutory(WikiDbContext context, IUnitOfWork return await query.LongCountAsync(); } + public async Task PutChatHistoryAsync(string id, string content, string? chatShareId) + { + if (chatShareId.IsNullOrEmpty()) + { + await Context.ChatDialogHistorys.Where(x => x.Id == id) + .ExecuteUpdateAsync(x => + x.SetProperty(b => b.Content, content)); + } + else + { + var result = await Context.ChatShares.FirstOrDefaultAsync(x => x.Id == chatShareId); + + if (result == null) + { + throw new UserFriendlyException("Ի"); + } + + await Context.ChatDialogHistorys.Where(x => x.Id == id) + .ExecuteUpdateAsync(x => + x.SetProperty(b => b.Content, content)); + } + } + private IQueryable CreateChatShareQueryable(Guid userId, string chatApplicationId) { return Context.ChatShares.AsNoTracking() diff --git a/src/Service/FastWiki.Service/Dockerfile b/src/Service/FastWiki.Service/Dockerfile index f5fdc8c0bb958e958a93c3e960f0c4d571c0b321..c85e81d514979e21e66098b523e701b3f0b86cac 100644 --- a/src/Service/FastWiki.Service/Dockerfile +++ b/src/Service/FastWiki.Service/Dockerfile @@ -20,7 +20,16 @@ FROM build AS publish ARG BUILD_CONFIGURATION=Release RUN dotnet publish "./FastWiki.Service.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM node:alpine as builder +WORKDIR /src +COPY ["web","."] +RUN npm i +RUN npm run build + + FROM base AS final WORKDIR /app COPY --from=publish /app/publish . +COPY --from=builder /src/dist ./wwwroot ENTRYPOINT ["dotnet", "FastWiki.Service.dll"] \ No newline at end of file diff --git a/src/Service/FastWiki.Service/Domain/ChatApplications/Repositories/IChatApplicationRepository.cs b/src/Service/FastWiki.Service/Domain/ChatApplications/Repositories/IChatApplicationRepository.cs index 6b05d705b1d0bca63fbd9a49101fc36c56823de3..5e7b9935ac3ba60254bd26cc49ad09f44d377489 100644 --- a/src/Service/FastWiki.Service/Domain/ChatApplications/Repositories/IChatApplicationRepository.cs +++ b/src/Service/FastWiki.Service/Domain/ChatApplications/Repositories/IChatApplicationRepository.cs @@ -147,4 +147,13 @@ public interface IChatApplicationRepository : IRepository /// Task GetSessionLogDialogCountAsync(string chatApplicationId); + + /// + /// 修改对话记录内容 + /// + /// + /// + /// + /// + Task PutChatHistoryAsync(string id,string content,string? chatShareId); } \ No newline at end of file diff --git a/src/Service/FastWiki.Service/Infrastructure/Helper/TokenHelper.cs b/src/Service/FastWiki.Service/Infrastructure/Helper/TokenHelper.cs index fbfde6ecbb50901b041f197906c26c81f1a39207..13a89b762c534da9208543fdc401b2595962b8c9 100644 --- a/src/Service/FastWiki.Service/Infrastructure/Helper/TokenHelper.cs +++ b/src/Service/FastWiki.Service/Infrastructure/Helper/TokenHelper.cs @@ -7,7 +7,7 @@ public static class TokenHelper private static GptEncoding? _encoding; /// - /// ȡGptEncoding + /// 获取GptEncoding /// /// public static GptEncoding GetGptEncoding() @@ -18,7 +18,7 @@ public static class TokenHelper } /// - /// token + /// 计算token /// /// /// diff --git a/src/Service/FastWiki.Service/Program.cs b/src/Service/FastWiki.Service/Program.cs index 7b0aa87a75841d381a8db56fbfb7ba61a9e2e442..5fb2f414e6d632775dc4b6968eb95d0ff756d231 100644 --- a/src/Service/FastWiki.Service/Program.cs +++ b/src/Service/FastWiki.Service/Program.cs @@ -1,5 +1,6 @@ using FastWiki.Service; using FastWiki.Service.Backgrounds; +using FastWiki.Service.Service; using Masa.Contrib.Authentication.Identity; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.FileProviders; @@ -24,7 +25,15 @@ builder builder .AddFastSemanticKernel(); -var app = builder.Services +var app = builder.Services.AddCors(options => + { + options.AddPolicy("AllowAll", + builder => builder + .SetIsOriginAllowed(_ => true) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials()); + }) .AddAuthorization() .AddHostedService() .AddJwtBearerAuthentication() @@ -82,13 +91,37 @@ var app = builder.Services .AddAutoInject() .AddServices(builder, option => option.MapHttpMethodsForUnmatched = ["Post"]); -app.UseMasaExceptionHandler(); +app.Use((async (context, next) => +{ + try + { + await next(context); + + if (context.Response.StatusCode == 404) + { + context.Request.Path = "/index.html"; + await next(context); + } + } + catch (UserFriendlyException userFriendlyException) + { + context.Response.StatusCode = 400; + + await context.Response.WriteAsJsonAsync(ResultDto.CreateError(userFriendlyException.Message, "400")); + } + catch (Exception e) + { + context.Response.StatusCode = 500; + + await context.Response.WriteAsJsonAsync(ResultDto.CreateError(e.Message, "500")); + } +})); var fileExtensionContentTypeProvider = new FileExtensionContentTypeProvider { Mappings = { - [".md"] = "application/octet-stream" + [".md"] = "application/octet-stream", } }; app.UseStaticFiles(new StaticFileOptions @@ -96,9 +129,9 @@ app.UseStaticFiles(new StaticFileOptions ContentTypeProvider = fileExtensionContentTypeProvider }); +app.UseCors("AllowAll"); app.UseAuthentication(); app.UseAuthorization(); - if (app.Environment.IsDevelopment()) { app.UseSwagger() @@ -117,4 +150,6 @@ if (app.Environment.IsDevelopment()) #endregion } +app.MapPost("/v1/chat/completions", OpenAIService.Completions); + app.Run(); \ No newline at end of file diff --git a/src/Service/FastWiki.Service/Service/ChatApplicationService.cs b/src/Service/FastWiki.Service/Service/ChatApplicationService.cs index 08b2ced8c36e4958ab2049036352b2647288e748..85822e93de0c9217d1cc4c493fc95135078feda6 100644 --- a/src/Service/FastWiki.Service/Service/ChatApplicationService.cs +++ b/src/Service/FastWiki.Service/Service/ChatApplicationService.cs @@ -1,4 +1,3 @@ -using DocumentFormat.OpenXml.Office2010.ExcelAc; using FastWiki.Service.Application.Storage.Queries; using FastWiki.Service.Domain.Storage.Aggregates; using FastWiki.Service.Infrastructure.Helper; @@ -457,4 +456,11 @@ public sealed class ChatApplicationService(WikiMemoryService wikiMemoryService, return query.Result; } + + public Task PutChatHistoryAsync(PutChatHistoryInput input) + { + var command = new PutChatHistoryCommand(input); + + return EventBus.PublishAsync(command); + } } diff --git a/src/Service/FastWiki.Service/Service/OpenAIService.cs b/src/Service/FastWiki.Service/Service/OpenAIService.cs new file mode 100644 index 0000000000000000000000000000000000000000..f115b0d8479dc177ed869b8e65dd2f0e152bfa86 --- /dev/null +++ b/src/Service/FastWiki.Service/Service/OpenAIService.cs @@ -0,0 +1,9 @@ +namespace FastWiki.Service.Service; + +public static class OpenAIService +{ + public static async Task Completions(HttpContext context) + { + + } +} \ No newline at end of file diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs new file mode 100644 index 0000000000000000000000000000000000000000..d6c953795300e4256c76542d6bb0fe06f08b5ad6 --- /dev/null +++ b/web/.eslintrc.cjs @@ -0,0 +1,18 @@ +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 }, + ], + }, +} diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a547bf36d8d11a4f89c59c144f24795749086dd1 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000000000000000000000000000000000000..0d6babeddbdbc9d9ac5bd4d57004229d22dbd864 --- /dev/null +++ b/web/README.md @@ -0,0 +1,30 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default { + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +} +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000000000000000000000000000000000000..a768892af176da8c144296c6abb9b81cc0379c27 --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + + Fast Wiki智能知识库 + + +
+ + + diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000000000000000000000000000000000000..09a768593eb8c33651ce430d3ddfc64e4801c32a --- /dev/null +++ b/web/package.json @@ -0,0 +1,31 @@ +{ + "name": "web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@lobehub/ui": "^1.132.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.22.3", + "styled-components": "^6.1.8" + }, + "devDependencies": { + "@types/react": "^18.2.56", + "@types/react-dom": "^18.2.19", + "@typescript-eslint/eslint-plugin": "^7.0.2", + "@typescript-eslint/parser": "^7.0.2", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.56.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "typescript": "^5.2.2", + "vite": "^5.1.4" + } +} diff --git a/web/public/model.json b/web/public/model.json new file mode 100644 index 0000000000000000000000000000000000000000..77cb5ec2dd6b1ef7ecacecef5bb38a07eca1863b --- /dev/null +++ b/web/public/model.json @@ -0,0 +1,38 @@ +{ + "chatModel": [ + { + "label": "gpt-3.5-turbo", + "value": "gpt-3.5-turbo" + }, + { + "label": "gpt-4-0125-preview", + "value": "gpt-4-0125-preview" + }, + { + "label": "gpt-4-1106-preview", + "value": "gpt-4-1106-preview" + }, + { + "label": "gpt-4-1106-vision-preview", + "value": "gpt-4-1106-vision-preview" + }, + { + "label": "gpt-4", + "value": "gpt-4" + }, + { + "label": "gpt-4-32k", + "value": "gpt-4-32k" + }, + { + "label": "gpt-3.5-turbo-0125", + "value": "gpt-3.5-turbo-0125" + } + ], + "embeddingModel": [ + { + "label": "text-embedding-3-small", + "value": "text-embedding-3-small" + } + ] + } \ No newline at end of file diff --git a/web/public/vite.svg b/web/public/vite.svg new file mode 100644 index 0000000000000000000000000000000000000000..e7b8dfb1b2a60bd50538bec9f876511b9cac21e3 --- /dev/null +++ b/web/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/App.css b/web/src/App.css new file mode 100644 index 0000000000000000000000000000000000000000..6f2503fb750615505485b942496455dcf4302916 --- /dev/null +++ b/web/src/App.css @@ -0,0 +1,19 @@ +#root{ + min-height:auto !important; + height: 100% !important; +} + +body{ + height: 100% !important; + min-height: auto !important; +} + +html{ + height: 100% !important; + min-height: auto !important; +} + +.ant-app{ + height: 100% !important; + min-height: auto !important; +} \ No newline at end of file diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000000000000000000000000000000000000..49cbd00aabc27c6c25e9c7138d1bb57ae6dd6be6 --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,45 @@ +import './App.css' +import { RouterProvider, createBrowserRouter } from 'react-router-dom' +import MainLayout from './layouts/main-layout' +import Home from './pages/home/page' +import Login from './pages/login/page' +import App from './pages/app/page' + +import { ThemeProvider } from '@lobehub/ui' +import Wiki from './pages/wiki/page' +import Chat from './pages/chat/page' +import User from './pages/user/page' +import AppDetail from './pages/app-detail/page' +import WikiDetail from './pages/wiki-detail/page' +import ShareChat from './pages/share-chat/page' + + +const router = createBrowserRouter([{ + path: '/', + element: +}, { + element: , + children: [ + { path: '/app', element: }, + { path: '/app/:id', element: }, + { path: '/wiki', element: }, + { path: '/wiki/:id', element: }, + { path: '/chat', element: }, + { path: '/user', element: }, + ] +}, { + path: '/login', + element: +}, { + path: '/share-chat', + element: +}]) + + +export default function AppPage() { + return ( + + + + ) +} diff --git a/web/src/assets/react.svg b/web/src/assets/react.svg new file mode 100644 index 0000000000000000000000000000000000000000..6c87de9bb3358469122cc991d5cf578927246184 --- /dev/null +++ b/web/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/components/ResponsiveIndex.ts b/web/src/components/ResponsiveIndex.ts new file mode 100644 index 0000000000000000000000000000000000000000..131a11181c593d5f40c739e64e315cc33b3cda1a --- /dev/null +++ b/web/src/components/ResponsiveIndex.ts @@ -0,0 +1,5 @@ +export function isMobileDevice() { + const userAgent = navigator.userAgent; + const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i; + return mobileRegex.test(userAgent) || window.innerWidth <= 768; +} \ No newline at end of file diff --git a/web/src/config.ts b/web/src/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..0748973c691b40215099caedc506587e93e14dc9 --- /dev/null +++ b/web/src/config.ts @@ -0,0 +1,7 @@ +// 获取环境变量 +export const config = { + FAST_API_URL: import.meta.env.VITE_FAST_API_URL ?? "", + NODE_ENV: import.meta.env.MODE, + DEV: import.meta.env.DEV, + VITE_VERSIONS: import.meta.env.VITE_VERSIONS ?? "v1", +}; diff --git a/web/src/index.css b/web/src/index.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/web/src/layouts/(desktop)/index.tsx b/web/src/layouts/(desktop)/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..632278b4b388ba2fdb67fdd13da20503d62ff64e --- /dev/null +++ b/web/src/layouts/(desktop)/index.tsx @@ -0,0 +1,78 @@ +import { ActionIcon, Logo, SideNav, Tooltip } from "@lobehub/ui"; +import { Album, Settings2, Box, User, BotMessageSquare } from 'lucide-react'; +import { memo, useEffect, useState } from "react"; +import { Flexbox } from 'react-layout-kit'; +import { Outlet } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; + +const DesktopLayout = memo(() => { + const [tab, setTab] = useState('chat'); + const navigate = useNavigate(); + + const tabs = [{ + icon: BotMessageSquare, + key: 'chat', + description: '聊天', + path: '/chat' + }, { + icon: Box, + key: 'application', + description: '应用', + path: '/app' + }, { + icon: Album, + key: 'wiki', + description: '知识库', + path: '/wiki' + }, { + icon: User, + key: 'user', + description: '用户管理', + path: '/user' + }] + + useEffect(()=>{ + // 获取当前路由匹配前缀一致的tab + const currentTab = tabs.find(item => window.location.pathname.startsWith(item.path)); + if(currentTab) { + setTab(currentTab.key); + } + + }) + + function updateTab(item: { key: string, path: string, description: string, icon: any }) { + setTab(item.key); + navigate(item.path); + } + + return ( + } + bottomActions={} + topActions={ + <> + {tabs.map((item,index) => { + return ( + + updateTab(item)} + size="large" + /> + ) + })} + + } + > + + + ) +}); + +export default DesktopLayout; \ No newline at end of file diff --git a/web/src/layouts/(mobile)/index.tsx b/web/src/layouts/(mobile)/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8290762026625a19aaad08927a7232720c4ed386 --- /dev/null +++ b/web/src/layouts/(mobile)/index.tsx @@ -0,0 +1,76 @@ +import { Icon, MobileTabBar } from "@lobehub/ui"; +import { memo, useEffect, useState } from "react"; +import { Album, Box, User, BotMessageSquare } from 'lucide-react'; +import { Flexbox } from 'react-layout-kit'; +import { Outlet } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; +import styled from 'styled-components'; + +const ActiveTitle = styled.span` + color: #1890ff; +`; + + +const MobileLayout = memo(() => { + const navigate = useNavigate(); + const [key, setTab] = useState('chat'); + + const items = [ + { + icon: , + key: 'chat', + title: key === 'chat' ? 聊天 : '聊天', + path: '/chat', + onClick: () => { + navigate('/chat'); + } + }, + { + icon: , + key: 'application', + path: '/app', + title: key === 'application' ? 应用 : '应用', + onClick: () => { + navigate('/app'); + } + }, + { + icon: , + key: 'wiki', + title: key === 'wiki' ? 知识库 : '知识库', + path: '/wiki', + onClick: () => { + navigate('/wiki'); + } + }, + { + icon: , + key: 'user', + title: key === 'user' ? 用户管理 : '用户管理', + path: '/user', + onClick: () => { + navigate('/user'); + } + }, + ]; + + useEffect(() => { + // 获取当前路由匹配前缀一致的tab + const currentTab = items.find(item => window.location.pathname.startsWith(item.path)); + if (currentTab) { + setTab(currentTab.key); + } + + }) + + return ( + + + ) +}); + +export default MobileLayout; \ No newline at end of file diff --git a/web/src/layouts/adaptive-layout.tsx b/web/src/layouts/adaptive-layout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3d967c39a8f097a638a32f0389612e4af857553b --- /dev/null +++ b/web/src/layouts/adaptive-layout.tsx @@ -0,0 +1,28 @@ +import { useState, useEffect } from 'react'; +import { isMobileDevice } from '../components/ResponsiveIndex'; + +interface IAdaptiveLayoutProps { + DesktopPage: React.ComponentType; + MobilePage: React.ComponentType; +} + +export default function AdaptiveLayout({DesktopPage, MobilePage}: IAdaptiveLayoutProps) { + const [isMobile, setIsMobile] = useState(isMobileDevice()); + + useEffect(() => { + const handleResize = () => { + setIsMobile(isMobileDevice()); + }; + + window.addEventListener('resize', handleResize); + + // 清除事件监听器 + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); // 空数组表示这个useEffect只在组件挂载和卸载时运行 + + const PageComponent = isMobile ? MobilePage : DesktopPage; + + return ; +} \ No newline at end of file diff --git a/web/src/layouts/main-layout.tsx b/web/src/layouts/main-layout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..129cc4311f7a3d0de4b86b8d8882f4660ab6c5c2 --- /dev/null +++ b/web/src/layouts/main-layout.tsx @@ -0,0 +1,25 @@ +import { useState, useEffect } from 'react'; +import DesktopPage from './(desktop)'; +import MobilePage from './(mobile)'; +import { isMobileDevice } from '../components/ResponsiveIndex'; + +export default function Login() { + const [isMobile, setIsMobile] = useState(isMobileDevice()); + + useEffect(() => { + const handleResize = () => { + setIsMobile(isMobileDevice()); + }; + + window.addEventListener('resize', handleResize); + + // 清除事件监听器 + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); // 空数组表示这个useEffect只在组件挂载和卸载时运行 + + const PageComponent = isMobile ? MobilePage : DesktopPage; + + return ; +} \ No newline at end of file diff --git a/web/src/main.tsx b/web/src/main.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ff8cf01cf4d6a5c26cbedc735066ee0aad663f15 --- /dev/null +++ b/web/src/main.tsx @@ -0,0 +1,6 @@ +import ReactDOM from 'react-dom/client' +import App from './App.tsx' + +ReactDOM.createRoot(document.getElementById('root')!).render( + , +) diff --git a/web/src/models/index.d.ts b/web/src/models/index.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..a1350de920bbd62ea228f0b2695da9710aae7b62 --- /dev/null +++ b/web/src/models/index.d.ts @@ -0,0 +1,204 @@ +export interface ChatApplicationDto { + id: string; + name: string; + prompt: string; + chatModel: string; + temperature: number; + maxResponseToken: number; + template: string; + parameter: { [key: string]: string; }; + opener: string; + wikiIds: number[]; + referenceUpperLimit: number; + noReplyFoundTemplate: string | null; + showSourceFile: boolean; + relevancy: number; +} + +export interface ChatDialogDto { + id: string; + name: string; + chatId: string; + description: string; + type: ChatDialogType; + creationTime: string; + typeName: string; + isEdit: boolean; +} + +export interface ChatDialogHistoryDto { + id: string; + chatDialogId: string; + content: string; + tokenConsumption: number; + current: boolean; + type: ChatDialogHistoryType; + sourceFile: SourceFileDto[]; + creationTime: string; +} + +export interface ChatShareCompletionsInput { + guestId: string; + chatDialogId: string; + chatShareId: string; + content: string; +} + + +export interface ChatShareDto { + id: string; + name: string; + chatApplicationId: string; + expires: string; + availableToken: number; + availableQuantity: number; +} +export interface CompletionsDto { + content: string; + sourceFile: SourceFileDto[]; +} + +export interface SourceFileDto { + name: string; + filePath: string; + fileId: string; +} + +export interface CompletionsInput { + chatDialogId: string; + chatId: string; + content: string; +} + +export interface CreateChatApplicationInput { + name: string; +} + + +export interface CreateChatDialogHistoryInput { + chatDialogId: string; + content: string; + current: boolean; + type: ChatDialogHistoryType; +} + +export interface CreateChatDialogInput { + name: string; + chatId: string; + description: string; + applicationId: string; + type: ChatDialogType; +} + + +export interface CreateChatShareInput { + name: string; + chatApplicationId: string; + expires: string; + availableToken: number; + availableQuantity: number; +} + +export interface SourceFileItem { + fileName: string; + content: string; +} + +export interface UpdateChatApplicationInput { + id: string; + name: string; + prompt: string; + chatModel: string; + temperature: number; + maxResponseToken: number; + template: string; + parameter: { [key: string]: string; }; + opener: string; + noReplyFoundTemplate: string | null; + showSourceFile: boolean; + wikiIds: number[]; +} + +export enum ChatDialogType { + ChatApplication, + ChatShare +} + +export interface CreateWikiDetailDataInput { + wikiId: number; + name: string; + fileId: number; + filePath: string; + state: string; + subsection: number; + mode: ProcessMode; + trainingPattern: TrainingPattern; +} + +export interface CreateWikiDetailsInput { + wikiId: number; + name: string; + fileId: number; + filePath: string; + subsection: number; + mode: ProcessMode; + trainingPattern: TrainingPattern; +} + + +export interface CreateWikiDetailWebPageInput { + wikiId: number; + name: string; + path: string; + state: string; + subsection: number; + mode: ProcessMode; + trainingPattern: TrainingPattern; +} + +export interface PaginatedListBase { + total: number; + result: TEntity[]; +} + +export interface WikiDto { + id: number; + icon: string; + name: string; + model: string; + embeddingModel: string; +} + +interface ChatModel { + label: string; + value: string; +} + +interface EmbeddingModel { + label: string; + value: string; +} + +interface Models { + chatModel: ChatModel[]; + embeddingModel: EmbeddingModel[]; +} + + +export enum TrainingPattern { + Subsection, + QA +} + +export enum ProcessMode { + Auto, + Custom +} +export interface WikiDetailVectorQuantityDto { + id: string; + content: string; + document_Id: string; + wikiDetailId: string; + fileId: string; + index: number; +} \ No newline at end of file diff --git a/web/src/pages/app-detail/(desktop)/index.tsx b/web/src/pages/app-detail/(desktop)/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..55571467d601b645c237262acbf0e7f40d164e08 --- /dev/null +++ b/web/src/pages/app-detail/(desktop)/index.tsx @@ -0,0 +1,97 @@ +import { memo, useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import AppDetailInfo from '../feautres/AppDetailInfo'; +import { Button } from 'antd'; + +import styled from 'styled-components'; +import { GetChatApplications } from '../../../services/ChatApplicationService'; +import SessionLog from '../feautres/SessionLog'; +import ReleaseApplication from '../feautres/ReleaseApplication'; + +const LeftTabs = styled.div` + width: 140px; + height: 100%; + padding: 20px; + /* 右边需要有一个分割线 */ + border-right: 1px solid #464545; +`; + + +export default memo(() => { + const { id } = useParams<{ id: string }>(); + if (id === undefined) return (
id is undefined
) + + const [application, setApplication] = useState({} as any); + + const [tabs, setTabs] = useState([] as any[]); + + const [tab, setTab] = useState() as any; + + useEffect(() => { + loadingApplication(); + }, [id]); + + async function loadingApplication() { + GetChatApplications(id as string) + .then((application) => { + setApplication(application); + }); + } + + useEffect(() => { + loadingTabs(); + }, [application]); + + function loadingTabs() { + const tabs = [{ + key: 1, + label: '应用配置' + }, { + key: 2, + label: '对话记录' + }, { + key: 3, + label: '发布应用' + }]; + + changeTab(tabs[0]); + + //强制刷新 + setTabs([...tabs]); + } + + function changeTab(key: any) { + setTab(key); + } + + return ( + <> +
+ + {tabs.map((item, index) => { + return + })} + +
+
+ { + tab?.key === 1 ? : ( + tab?.key === 2 ? : + ( + tab?.key === 3 ? : null + ) + ) + } +
+ + ); +}) \ No newline at end of file diff --git a/web/src/pages/app-detail/(mobile)/index.tsx b/web/src/pages/app-detail/(mobile)/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..dd6c239a6b8f8322b5714f118b4b7fd3629522ad --- /dev/null +++ b/web/src/pages/app-detail/(mobile)/index.tsx @@ -0,0 +1,35 @@ +import { Tabs } from 'antd'; +import { memo } from 'react'; +// import { useParams } from 'react-router-dom'; + + +export default memo(() => { + // const { id } = useParams<{ id: string }>(); + + return ( +
+
+ 基本信息
+ }, + { + label: '对话记录', + key: '1', + children:
基本信息
+ }, + { + label: '发布应用', + key: '1', + children:
基本信息
+ } + ]} + /> +
+ + ); +}) \ No newline at end of file diff --git a/web/src/pages/app-detail/feautres/AppDetailInfo.tsx b/web/src/pages/app-detail/feautres/AppDetailInfo.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c72046474cf2c104c706dbc29003e40ee56abca2 --- /dev/null +++ b/web/src/pages/app-detail/feautres/AppDetailInfo.tsx @@ -0,0 +1,308 @@ +import { memo, useEffect, useState } from "react"; +import { getModels } from "../../../store/Model"; +import { Select, Row, Checkbox, Button, Collapse, Col, Slider, message } from 'antd'; +import styled from 'styled-components'; +import { ChatApplicationDto } from "../../../models"; +import { PutChatApplications } from "../../../services/ChatApplicationService"; +import { GetWikisList } from "../../../services/WikiService"; + +interface IAppDetailInfoProps { + value: ChatApplicationDto +} + +const Container = styled.div` + display: grid; + padding: 20px; + /* 屏幕居中显示 */ + margin: auto; + width: 580px; + overflow: auto; + height: 100%; + + // 隐藏滚动条 + &::-webkit-scrollbar { + display: none; + } + scrollbar-width: none; + +`; + +const ListItem = styled.div` + display: flex; + justify-content: space-between; + padding: 20px; + width: 100%; +`; + +const AppDetailInfo = memo(({ value }: IAppDetailInfoProps) => { + if (value === undefined) return null; + + const [model, setModel] = useState([] as any[]); + const [wiki, setWiki] = useState([] as any[]); + const [input,] = useState({ + keyword: '', + page: 1, + pageSize: 10 + } as any); + + useEffect(() => { + getModels() + .then((models) => { + setModel(models.chatModel.map((item) => { + return { label: item.label, value: item.value } + })); + }); + + loadingWiki(); + + }, []); + + function loadingWiki() { + // TODO: 暂时写死 + GetWikisList(input.keyword, input.page, 100) + .then((wiki) => { + setWiki(wiki.result); + }); + } + + const [application, setApplication] = useState(value); + + useEffect(() => { + setApplication(value); + }, [value]); + + function save() { + PutChatApplications(application) + .then(() => { + message.success('保存成功'); + }); + } + + return ( + + + 对话模型 + + + + 提示词 + + + + 未找到回复模板 + + + + + + }]} + /> + { + setApplication({ + ...application, + showSourceFile: v.target.checked + }); + }}>是否启用引用来源显示 + + + ) +}) + +export default AppDetailInfo; \ No newline at end of file diff --git a/web/src/pages/app-detail/feautres/CreateApplication.tsx b/web/src/pages/app-detail/feautres/CreateApplication.tsx new file mode 100644 index 0000000000000000000000000000000000000000..feeb450cda9189862d0025f7143cbc8f464c6377 --- /dev/null +++ b/web/src/pages/app-detail/feautres/CreateApplication.tsx @@ -0,0 +1,76 @@ +import { Modal } from "@lobehub/ui"; +import { Form, Input, Button, message } from 'antd'; +import { CreateShare } from "../../../services/ChatApplicationService"; +interface CreateApplicationProps { + visible: boolean; + id: string; + onClose: () => void; + onSuccess: () => void; + +} + +export default function CreateApplication({ visible, onClose, onSuccess,id }: CreateApplicationProps) { + + return ( +
{ + if(values.availableToken === undefined) { + values.availableToken = -1; + } + if(values.availableQuantity === undefined) { + values.availableQuantity = -1; + } + values.chatApplicationId = id; + + await CreateShare(values); + message.success('创建成功'); + onSuccess(); + }} + onFinishFailed={(errorInfo) => { + console.log('Failed:', errorInfo); + }} + autoComplete="off" + > + + + + + + + + + + + + + + + + + +
+
) +} \ No newline at end of file diff --git a/web/src/pages/app-detail/feautres/ReleaseApplication.tsx b/web/src/pages/app-detail/feautres/ReleaseApplication.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a6a095c874728bc95a400c67133e181b2d22fc26 --- /dev/null +++ b/web/src/pages/app-detail/feautres/ReleaseApplication.tsx @@ -0,0 +1,131 @@ +import { memo, useEffect, useState } from "react"; +import { ChatShareDto } from "../../../models"; +import type { TableProps } from 'antd'; +import { Button, Table ,message } from 'antd'; +import styled from 'styled-components'; +import CreateApplication from "./CreateApplication"; +import { GetChatShareList } from "../../../services/ChatApplicationService"; +import { copyToClipboard } from "../../../utils/stringHelper"; + + +const Title = styled.div` + font-size: 30px; + font-weight: 600; + margin-bottom: 16px; + +`; +interface IReleaseApplicationProps { + id: string; +} + +export default memo((props: IReleaseApplicationProps) => { + const columns: TableProps['columns'] = [ + { + title: '文件名', + dataIndex: 'name', + key: 'name', + }, + { + title: '过期时间', + dataIndex: 'expires', + key: 'expires', + }, + { + title: '对话类型', + dataIndex: 'typeName', + key: 'typeName', + }, + { + title: '可用Token数量', + dataIndex: 'availableToken', + key: 'availableToken', + }, + { + title: '可用数量', + dataIndex: 'availableQuantity', + key: 'availableQuantity', + }, + { + title: '操作', + key: 'action', + render: (_, item) => ( + + ), + }, + ]; + + const [data, setData] = useState([]); + const [visible, setVisible] = useState(false); + const [input, setInput] = useState({ + page: 1, + pageSize: 10, + chatApplicationId: props.id + } as any); + + const [total, setTotal] = useState(0); + useEffect(() => { + setInput({ + ...input, + chatApplicationId: props.id + }); + + }, [props.id]) + + function handleTableChange(page: number, pageSize: number) { + setInput({ + ...input, + page: page, + pageSize: pageSize, + }); + } + + function loadingData() { + GetChatShareList(input.chatApplicationId, input.page, input.pageSize) + .then((result) => { + setData(result.result); + setTotal(result.total); + }) + } + + useEffect(() => { + loadingData(); + }, [input]); + + + return (<> + + 发布应用 + <Button + onClick={() => setVisible(true)} + style={{ + float: 'inline-end', + position: 'absolute', + right: 16, + }} + >发布</Button> + + + setVisible(false)} + onSuccess={() => { + setInput({ + ...input, + page: 1 + }); + setVisible(false); + }} + /> + + ); +}); \ No newline at end of file diff --git a/web/src/pages/app-detail/feautres/SessionLog.tsx b/web/src/pages/app-detail/feautres/SessionLog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a5f81d3f50e18f177cf4484c7b379ced9312e5a9 --- /dev/null +++ b/web/src/pages/app-detail/feautres/SessionLog.tsx @@ -0,0 +1,102 @@ +import { ChatDialogDto } from "../../../models"; +import type { TableProps } from 'antd'; +import { Button, Table } from 'antd'; +import { useEffect, useState } from "react"; +import { GetSessionLogDialog } from "../../../services/ChatApplicationService"; +import styled from 'styled-components'; + + +const Title = styled.div` + font-size: 30px; + font-weight: 600; + margin-bottom: 16px; + +`; + +interface ISessionLogProps { + id: string; +} + +export default function SessionLog({ id }: ISessionLogProps) { + const columns: TableProps['columns'] = [ + { + title: '对话名称', + dataIndex: 'name', + key: 'name', + }, + { + title: '对话描述', + dataIndex: 'description', + key: 'description', + }, + { + title: '对话类型', + dataIndex: 'typeName', + key: 'typeName', + }, + { + title: '对话创建时间', + dataIndex: 'creationTime', + key: 'creationTime', + }, + { + title: 'Action', + key: 'action', + render: () => ( + + ), + }, + ]; + + const [data, setData] = useState([]); + const [total, setTotal] = useState(0); + const [input, setInput] = useState({ + page: 1, + pageSize: 10, + chatApplicationId: id + } as any); + + useEffect(() => { + setInput({ + ...input, + chatApplicationId: id + }); + }, [id]); + + function loadingData() { + GetSessionLogDialog(input.chatApplicationId, input.page, input.pageSize) + .then((result) => { + setData(result.result); + setTotal(result.total); + }) + } + + useEffect(() => { + loadingData(); + }, [input]); + + function handleTableChange(page: number, pageSize: number) { + + setInput({ + ...input, + page: page, + pageSize: pageSize, + }); + } + + return ( + <> + + 对话日志 + +
+ ) +} \ No newline at end of file diff --git a/web/src/pages/app-detail/page.tsx b/web/src/pages/app-detail/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..afb28f262831fe32310f829b02726ff482c5711d --- /dev/null +++ b/web/src/pages/app-detail/page.tsx @@ -0,0 +1,8 @@ +import DesktopPage from './(desktop)'; +import MobilePage from './(mobile)'; +import AdaptiveLayout from '../../layouts/adaptive-layout'; + +export default function AppDetail() { + + return ; +} \ No newline at end of file diff --git a/web/src/pages/app/(desktop)/index.tsx b/web/src/pages/app/(desktop)/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0c118c12931a37ff00254684af1226fa7f58e063 --- /dev/null +++ b/web/src/pages/app/(desktop)/index.tsx @@ -0,0 +1,31 @@ +import { memo, useState } from "react"; +import Header from "../features/Header"; +import { AppList } from "../features/AppList"; +import { Layout } from "@lobehub/ui"; + +const DesktopLayout = memo(() => { + const [input, setInput] = useState({ + page: 1, + pageSize: 12 + }); + return ( +
+ { + setInput({ + ...input, + page: 1 + }); + }}/> + } + > + { + setInput(v); + }} input={input}/> + +
) +}); + +export default DesktopLayout; \ No newline at end of file diff --git a/web/src/pages/app/(mobile)/index.tsx b/web/src/pages/app/(mobile)/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..65786f8e89d06384cf38792170f117a1a4705c20 --- /dev/null +++ b/web/src/pages/app/(mobile)/index.tsx @@ -0,0 +1,16 @@ +import { memo, useState } from "react"; +import { AppList } from "../features/AppList"; + +const MobileLayout = memo(() => { + const [input, setInput] = useState({ + page: 1, + pageSize: 10 + }); + return (<> + { + setInput(v); + }} input={input}/> + ) +}); + +export default MobileLayout; \ No newline at end of file diff --git a/web/src/pages/app/features/AppList.tsx b/web/src/pages/app/features/AppList.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cfcfe7aadb349c2f4e248833fd27b8f0985cf80b --- /dev/null +++ b/web/src/pages/app/features/AppList.tsx @@ -0,0 +1,120 @@ +import { GridShowcase, LogoThree, SpotlightCard } from '@lobehub/ui'; +import { useEffect, useState } from 'react'; +import { DeleteChatApplications, GetChatApplicationsList } from '../../../services/ChatApplicationService'; +import { Flexbox } from 'react-layout-kit'; +import { message, Button, Pagination } from 'antd'; +import styled from 'styled-components'; +import { DeleteOutlined } from '@ant-design/icons'; +import { ChatApplicationDto } from '../../../models'; +import { useNavigate } from 'react-router-dom'; + + +const AppItemDetail = styled.div` + padding: 16px; +`; + +interface IAppListProps { + input: { + page: number; + pageSize: number; + } + setInput: (input: any) => void; +} + +export function AppList(props: IAppListProps) { + const navigate = useNavigate(); + + const [data, setData] = useState([]); + const [total, setTotal] = useState(0); + + const render = (item: ChatApplicationDto) => ( + + { + openAppDetail(item.id) + }} style={{ + width: '100%', + }}> +
+ {item.name} +
+
+ + 对话模型: + {item.chatModel} + +
+
+ + + + + ) +} \ No newline at end of file diff --git a/web/src/pages/app/features/Header.tsx b/web/src/pages/app/features/Header.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4dc5e016b1851526ed988fafc4ad611153f2e313 --- /dev/null +++ b/web/src/pages/app/features/Header.tsx @@ -0,0 +1,23 @@ +import { Button } from 'antd'; + +import { Header } from '@lobehub/ui'; +import { CreateApp } from './CreateApp'; +import { useState } from 'react'; + +interface IAppHeaderProps { + onSucess: () => void; +} + +export default function AppHeader(props:IAppHeaderProps) { + const [visible, setVisible] = useState(false); + const onClose = () => setVisible(false); + return ( + <> +
setVisible(true)}>新增} nav={'应用管理'} /> + { + props.onSucess(); + onClose(); + })}/> + + ) +} \ No newline at end of file diff --git a/web/src/pages/app/page.tsx b/web/src/pages/app/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..466915e9783797038ff4e2b7091c4570ef20ee1d --- /dev/null +++ b/web/src/pages/app/page.tsx @@ -0,0 +1,7 @@ +import DesktopPage from './(desktop)'; +import MobilePage from './(mobile)'; +import AdaptiveLayout from '../../layouts/adaptive-layout'; + +export default function Login() { + return ; +} \ No newline at end of file diff --git a/web/src/pages/chat/(desktop)/index.tsx b/web/src/pages/chat/(desktop)/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b46b9fe01eb62977c14118b47531780c2af1e62f --- /dev/null +++ b/web/src/pages/chat/(desktop)/index.tsx @@ -0,0 +1,460 @@ +import { ChatList, DraggablePanel, Tooltip, } from "@lobehub/ui"; +import { Select } from 'antd'; +import { useEffect, useState } from "react"; +import { CreateChatDialog, CreateChatDialogHistory, DeleteDialog, DeleteDialogHistory, GetChatApplicationsList, GetChatDialog, GetChatDialogHistory, PutChatHistory } from "../../../services/ChatApplicationService"; +import Divider from "@lobehub/ui/es/Form/components/FormDivider"; +import { Button, message } from 'antd' +import { useNavigate } from "react-router-dom"; +import styled from "styled-components"; +import { DeleteOutlined } from '@ant-design/icons'; +import { + ActionIcon, + ChatInputActionBar, + ChatInputArea, + ChatSendButton, +} from '@lobehub/ui'; +import { + ActionsBar, + ChatListProps, +} from '@lobehub/ui'; + +import { Eraser, Languages } from 'lucide-react'; +import { Flexbox } from 'react-layout-kit'; +import { fetchRaw } from "../../../utils/fetch"; +import CreateDialog from "../feautres/CreateDialog"; + +const DialogList = styled.div` + margin-top: 8px; + padding: 8px; + overflow: auto; + height: calc(100vh - 110px); +`; + +const DialogItem = styled.div` + padding: 8px; + border: 1px solid #d9d9d9; + border-radius: 8px; + cursor: pointer; + margin-bottom: 8px; + transition: border-color 0.3s linear; + &:hover { + border-color: #1890ff; + } + + // 当组件被选中时修改样式 + &.selected { + border-color: #1890ff; + } +`; + +export default function DesktopLayout() { + const navigate = useNavigate(); + const [applications, setApplications] = useState([] as any[]); + const [application, setApplication] = useState(null as any); + const [dialogs, setDialogs] = useState([] as any[]); + const [createDialogVisible, setCreateDialogVisible] = useState(false); + const [dialog, setDialog] = useState({} as any); + const [history, setHistory] = useState([] as any[]); + const [value, setValue] = useState('' as string); + const [loading, setLoading] = useState(false); + const [input] = useState({ + page: 1, + pageSize: 5 + }); + + async function loadingApplications() { + try { + const result = await GetChatApplicationsList(1, 1000); + setApplications(result.result); + if (result.total === 0) { + message.error('您还没有应用,请先创建应用'); + // 等待1秒后跳转 + setTimeout(() => { + navigate('/app'); + }, 1000); + return; + } + setApplication(result.result[0]); + } catch (error) { + + } + } + + async function loadingDialogs() { + try { + + const result = (await GetChatDialog(application.id, true)) as any[]; + setDialogs(result); + if (result.length === 0) { + await AddChatDialog({ + name: '默认对话', + description: '默认创建的对话', + applicationId: application.id, + type: 0 + }) + loadingDialogs(); + return; + } + setDialog(result[0]); + } catch (error) { + + } + } + + async function LoadingSession() { + try { + const result = await GetChatDialogHistory(dialog.id, input.page, input.pageSize); + + const history = result.result.map((item: any) => { + return { + content: item.content, + createAt: item.createAt, + extra: {}, + id: item.id, + meta: { + avatar: item.current ? "https://blog-simple.oss-cn-shenzhen.aliyuncs.com/Avatar.jpg" : "https://blog-simple.oss-cn-shenzhen.aliyuncs.com/chatgpt.png", + title: item.current ? "我" : "AI助手", + }, + role: item.current ? 'user' : 'assistant', + }; + }); + + setHistory(history); + + // 等待1秒后滚动到底部 + setTimeout(() => { + const chatlayout = document.getElementById('chat-layout'); + if (chatlayout) { + chatlayout.scrollTop = chatlayout.scrollHeight; + } + }, 1000); + } catch (error) { + + } + } + + async function AddChatDialog(data: any) { + try { + await CreateChatDialog(data) + } catch (error) { + message.error('创建失败'); + } + } + + + useEffect(() => { + if (application) { + loadingDialogs(); + } + }, [application]); + + useEffect(() => { + if (dialog) { + LoadingSession(); + } + }, [dialog, input]); + + useEffect(() => { + loadingApplications(); + }, []); + + function handleChange(value: any) { + console.log(`selected ${value}`); + } + + const control: ChatListProps | any = + { + showTitle: false, + } + + async function sendChat() { + const v = value; + if(loading){ + return + } + + setLoading(true); + setValue(''); + + const chatlayout = document.getElementById('chat-layout'); + + history.push({ + content: v, + createAt: new Date().toISOString(), + extra: {}, + id: new Date().getTime(), + meta: { + avatar: "https://blog-simple.oss-cn-shenzhen.aliyuncs.com/Avatar.jpg", + title: "我", + }, + role: 'user', + }) + + setHistory([...history]); + + + let chat = { + content: '', + createAt: new Date().toISOString(), + extra: {}, + id: new Date().getTime(), + meta: { + avatar: "https://blog-simple.oss-cn-shenzhen.aliyuncs.com/chatgpt.png", + title: "AI助手", + }, + role: 'assistant', + }; + + setHistory([...history, chat]); + + // 滚动到底部 + if (chatlayout) { + chatlayout.scrollTop = chatlayout.scrollHeight; + } + + const stream = await fetchRaw('/api/v1/ChatApplications/Completions', { + chatDialogId: dialog.id, + content: v, + chatId: application.id + }); + + + for await (const c of stream) { + if (c) { + let content = c; + // 先匹配删除前缀 [ 和后缀 ] + if (content.startsWith('[') || content.startsWith(',')) { + // 删除第一个字符 + content = c.slice(1); + } + if (content.endsWith(']')) { + // 删除最后一个字符 + content = c.slice(0, c.length - 1); + } + if (content === '') { + return; + } + + if (content.startsWith(',') === true) { + content = content.slice(1); + } + + content = "[" + content + "]"; + + var obj = JSON.parse(content) as any[]; + + obj.forEach((item) => { + chat.content += item.content; + }); + + setHistory([...history, chat]); + + // 滚动到底部 + if (chatlayout) { + chatlayout.scrollTop = chatlayout.scrollHeight; + } + } + } + + await CreateChatDialogHistory({ + chatDialogId: dialog.id, + content: v, + current: true, + type: 0 + }) + + + await CreateChatDialogHistory({ + chatDialogId: dialog.id, + content: chat.content, + current: false, + type: 0 + }) + + + setLoading(false); + } + + function deleteDialog(id: string) { + DeleteDialog(id) + .then(() => { + loadingDialogs(); + }) + } + + async function ActionsClick(e: any, item: any) { + if (e.key === 'del') { + await DeleteDialogHistory(item.id) + message.success('删除成功'); + + const index = history.findIndex((i) => i.id === item.id); + history.splice(index, 1); + setHistory([...history]); + + } else if (e.key === 'regenerate') { + message.error('暂时并未支持重置!'); + } + } + + return <> + +
+ +
+ 请选择您的应用 +
+ { + setData({ + ...data, + name: e.target.value + }) + }} + placeholder="对话名称"/> +
+
+ { + setData({ + ...data, + description: e.target.value + }) + }} + placeholder="对话描述"/> +
+ + ) +} \ No newline at end of file diff --git a/web/src/pages/chat/page.tsx b/web/src/pages/chat/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4054bf6fcce6d3213bd19d60082b451ca441a9b3 --- /dev/null +++ b/web/src/pages/chat/page.tsx @@ -0,0 +1,8 @@ +import DesktopPage from './(desktop)'; +import MobilePage from './(mobile)'; +import AdaptiveLayout from '../../layouts/adaptive-layout'; + +export default function Chat() { + + return ; +} \ No newline at end of file diff --git a/web/src/pages/home/(desktop)/index.tsx b/web/src/pages/home/(desktop)/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6ba6b227d54a354b8b38851e4f3fcba9ecc10348 --- /dev/null +++ b/web/src/pages/home/(desktop)/index.tsx @@ -0,0 +1,89 @@ +import { Footer, FooterProps, Header, Layout, Logo, LogoProps, useControls, useCreateStore } from '@lobehub/ui'; +import { Button, Flex } from 'antd'; +import { Publicity } from '../features/Publicity'; + +export default () => { + const store = useCreateStore(); + const control: LogoProps | any = useControls( + { + size: { + max: 240, + min: 16, + step: 4, + value: 45, + }, + type: { + options: ['3d', 'flat', 'high-contrast', 'text', 'combine'], + value: '3d', + }, + }, + { store }, + ); + + const columns: FooterProps['columns'] = [ + { + items: [ + { + description: 'Fast Wiki文档', + openExternal: true, + title: 'Fast Wiki文档', + url: 'https://docs.token-ai.cn/', + }, + ], + title: '文档', + }, + { + items: [ + { + description: '商务合作邮箱', + openExternal: true, + title: '商务合作邮箱', + url: 'mailto:239573049@token-ai.cn', + }, + ], + title: '合作', + }, + ]; + + return ( +
+ + + + } + header={ +
} + nav={ + + + + + } + actions={ + + } + /> + } + > + + +
+ ); +}; \ No newline at end of file diff --git a/web/src/pages/home/(mobile)/index.tsx b/web/src/pages/home/(mobile)/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..904ef36adf4f3e15441cc4813eb0a53431c52fb3 --- /dev/null +++ b/web/src/pages/home/(mobile)/index.tsx @@ -0,0 +1,72 @@ +import { Footer, FooterProps, Header, Layout, Logo, LogoProps, useControls, useCreateStore } from '@lobehub/ui'; +import { Button, Flex } from 'antd'; +import { Publicity } from '../features/Publicity'; + +export default () => { + const store = useCreateStore(); + const control: LogoProps | any = useControls( + { + size: { + max: 240, + min: 16, + step: 4, + value: 45, + }, + type: { + options: ['3d', 'flat', 'high-contrast', 'text', 'combine'], + value: '3d', + }, + }, + { store }, + ); + + const columns: FooterProps['columns'] = [ + { + items: [ + { + description: '商务合作邮箱', + openExternal: true, + title: '商务合作邮箱', + url: 'mailto:239573049@token-ai.cn', + }, + ], + title: '合作', + }, + ]; + + return ( +
+ + + } + header={ +
+ + } + nav={ + } + actions={ + + } + /> + } + > + + +
+ ); +}; \ No newline at end of file diff --git a/web/src/pages/home/features/Publicity.tsx b/web/src/pages/home/features/Publicity.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7519741308e75fa200c9a3f18d5547e0791e265f --- /dev/null +++ b/web/src/pages/home/features/Publicity.tsx @@ -0,0 +1,117 @@ + +import styled from 'styled-components'; +import { Button } from 'antd'; +import { Features, FeaturesProps } from '@lobehub/ui'; +import { + GithubOutlined, + OpenAIOutlined, + RadarChartOutlined, + CoffeeOutlined, + BugOutlined +} from "@ant-design/icons"; + +const Title = styled.span` + font-size: 40px; + font-weight: bold; + text-align: center; + margin-top: 20px; + margin-bottom: 20px; + display: block; +`; + +const SubTitle = styled.span` + font-size: 20px; + text-align: center; + margin-top: 20px; + margin-bottom: 20px; + display: block; + +`; + +const GithubStar = styled.div` + margin-bottom: 20px; + display: flex; + justify-content: center; + align-items: center; + width: 100%; +`; + +const Slogan = styled.span` + font-size: 20px; + text-align: center; + margin-top: 20px; + margin-bottom: 20px; + display: block; +`; + +export function Publicity() { + const items: FeaturesProps['items'] = [ + { + description: + 'Fast Wiki项目代码开源,并且使用Apache-2.0协议,您可以自由使用、修改、分发Fast Wiki的代码。我们欢迎您的贡献!', + icon: GithubOutlined as any, + title: '更开放', + }, + { + description: + '支持GPT,Clude,ChatGLM等多种模型,满足您的更复杂的需求', + icon: OpenAIOutlined as any, + title: '支持多种模型', + }, + { + description: + '基于Semantic Kernel使您的应用更加智能的扩展,让您的知识库更加智能化', + icon: RadarChartOutlined, + title: '智能扩展', + }, + { + description: + 'Fast Wiki前端基于React+LobeHub UI,后端.NET8+MasaFramework,让代码更易维护和扩展', + icon: CoffeeOutlined, + title: '优雅的架构设计', + }, + { + description: + '拥有搜索测试,引用修改,数据AI分析等多种功能,让您的调试更加直观', + icon: BugOutlined, + title: '可视化调试', + }, + ]; + + return ( + <> + 让您的知识更智能 + 基于LLM大模型的智能化管理平台 + + + + + + 为什么使用Fast Wiki? + + + + ); +} \ No newline at end of file diff --git a/web/src/pages/home/page.tsx b/web/src/pages/home/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..466915e9783797038ff4e2b7091c4570ef20ee1d --- /dev/null +++ b/web/src/pages/home/page.tsx @@ -0,0 +1,7 @@ +import DesktopPage from './(desktop)'; +import MobilePage from './(mobile)'; +import AdaptiveLayout from '../../layouts/adaptive-layout'; + +export default function Login() { + return ; +} \ No newline at end of file diff --git a/web/src/pages/login/(desktop)/index.tsx b/web/src/pages/login/(desktop)/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c629b63fb89f7f2c9189ae63542aad4cbf9ac9be --- /dev/null +++ b/web/src/pages/login/(desktop)/index.tsx @@ -0,0 +1,90 @@ + +import { memo, useState } from 'react'; +import { Input, Button } from 'antd'; +import { EyeInvisibleOutlined, EyeTwoTone, GithubOutlined } from '@ant-design/icons'; +import { Footer, Logo, LogoProps, useControls, useCreateStore } from '@lobehub/ui'; +import styled from 'styled-components'; +import { handleLogin } from '../features/login'; + +const GithubButton = styled.span` + transition: all 0.3s; + display: inline-flex; + align-items: center; + cursor: pointer; + &:hover { + transition: all 0.3s; + color: #0366d6; + cursor: pointer; + } +`; + +const Login = memo(() => { + const [loading, setLoading] = useState(false); + const [user, setUser] = useState(''); + const [password, setPassword] = useState(''); + const store = useCreateStore(); + const control: LogoProps | any = useControls( + { + size: { + max: 240, + min: 16, + step: 4, + value: 64, + }, + type: { + options: ['3d', 'flat', 'high-contrast', 'text', 'combine'], + value: '3d', + }, + }, + { store }, + ); + + return ( + <> +
+
+ +
+
+ setUser(e.target.value)} + size='large' + placeholder="请输入账号" /> +
+
+ setPassword(e.target.value)} + size='large' + placeholder="请输入密码" + iconRender={visible => (visible ? : )} + /> +
+
+ +
+
+ + GitHub登录 + +
+
+ + ); +}); + +export default Login; \ No newline at end of file diff --git a/web/src/pages/login/(mobile)/index.tsx b/web/src/pages/login/(mobile)/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b2298e7b8615e7fd50131356df45b5955461a8ec --- /dev/null +++ b/web/src/pages/login/(mobile)/index.tsx @@ -0,0 +1,120 @@ +import { memo, useState } from "react"; +import { Input, Button } from "antd"; +import { EyeInvisibleOutlined, EyeTwoTone, GithubOutlined } from "@ant-design/icons"; +import styled from "styled-components"; +import { Logo, LogoProps, useControls, useCreateStore } from '@lobehub/ui'; +import { handleLogin } from "../features/login"; + +const LoginContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + margin-top: 40%; + width:100%; +`; + +const LogoContainer = styled.div` + /* 根据你的需求设置Logo的样式 */ +`; + +const InputContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + padding: 0 20px; + margin-top: 20px; +`; + +const StyledInputPassword = styled(Input.Password)` + width: 100%; + margin-bottom: 16px; /* Add margin bottom for spacing */ +`; + +const StyledInput = styled(Input)` + width: 100%; + margin-bottom: 16px; /* Add margin bottom for spacing */ +`; + + +const StyledButton = styled(Button)` + width: 100%; + margin-bottom: 16px; /* Add margin bottom for spacing */ +`; + +const GithubLogin = styled.div` + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s; + &:hover { + transition: all 0.3s; + color: #0366d6; + } + margin-top: 20px; +`; + +const MobileLayout = memo(() => { + const [loading, setLoading] = useState(false); + const [user, setUser] = useState(''); + const [password, setPassword] = useState(''); + const store = useCreateStore(); + const control: LogoProps | any = useControls( + { + size: { + max: 240, + min: 16, + step: 4, + value: 64, + }, + type: { + options: ['3d', 'flat', 'high-contrast', 'text', 'combine'], + value: '3d', + }, + }, + { store }, + ); + + return ( + + + + + + setUser(e.target.value)} + /> + setPassword(e.target.value)} + iconRender={(visible) => + visible ? : + } + /> + { + setLoading(true); + handleLogin(user, password,()=>{ + window.location.href = '/'; + }); + setLoading(false); + }} + size="large" type="primary" block> + 登录 + + + GitHub登录 + + + + ); +}); + +export default MobileLayout; \ No newline at end of file diff --git a/web/src/pages/login/features/login.ts b/web/src/pages/login/features/login.ts new file mode 100644 index 0000000000000000000000000000000000000000..47d44de1221919630bc505861c9c2804ecd8d077 --- /dev/null +++ b/web/src/pages/login/features/login.ts @@ -0,0 +1,13 @@ +import { login } from "../../../services/AuthorizeService"; +import { message } from "antd"; + +export function handleLogin(user: string, password: string, onSuccess: () => void) { + login(user, password).then((value) => { + message.success('登录成功'); + if (value.token) { + localStorage.setItem('token', value.token); + onSuccess() + } + }).catch(() => { + }); +} \ No newline at end of file diff --git a/web/src/pages/login/page.tsx b/web/src/pages/login/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e363cd5799b67a1ae7756ed257b0a640248aaabf --- /dev/null +++ b/web/src/pages/login/page.tsx @@ -0,0 +1,25 @@ +import { useState, useEffect } from 'react'; +import DesktopPage from './(desktop)'; +import MobilePage from './(mobile)'; +import { isMobileDevice } from '../../components/ResponsiveIndex'; + +export default function Login() { + const [isMobile, setIsMobile] = useState(isMobileDevice()); + + useEffect(() => { + const handleResize = () => { + setIsMobile(isMobileDevice()); + }; + + window.addEventListener('resize', handleResize); + + // 清除事件监听器 + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); // 空数组表示这个useEffect只在组件挂载和卸载时运行 + + const PageComponent = isMobile ? MobilePage : DesktopPage; + + return ; +} \ No newline at end of file diff --git a/web/src/pages/share-chat/(desktop)/index.tsx b/web/src/pages/share-chat/(desktop)/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6eb3d6ace7a9514dffa0e1af4bdd3b1a5cd77547 --- /dev/null +++ b/web/src/pages/share-chat/(desktop)/index.tsx @@ -0,0 +1,488 @@ +import { ChatList, DraggablePanel, Tooltip, } from "@lobehub/ui"; +import { Select } from 'antd'; +import { useEffect, useState } from "react"; +import { CreateChatDialog, CreateChatDialogHistory, DeleteDialog, DeleteDialogHistory, GetChatApplicationsList, GetChatDialog, GetChatDialogHistory, GetChatShareApplication, GetChatShareDialog, PutChatHistory } from "../../../services/ChatApplicationService"; +import Divider from "@lobehub/ui/es/Form/components/FormDivider"; +import { Button, message } from 'antd' +import { DeleteOutlined } from '@ant-design/icons'; +import { useNavigate } from "react-router-dom"; +import styled from "styled-components"; +import { + ActionIcon, + ChatInputActionBar, + ChatInputArea, + ChatSendButton, +} from '@lobehub/ui'; +import { + ActionsBar, + ChatListProps, +} from '@lobehub/ui'; + +import { Eraser, Languages } from 'lucide-react'; +import { Flexbox } from 'react-layout-kit'; +import { fetchRaw } from "../../../utils/fetch"; +import CreateDialog from "../feautres/CreateDialog"; +import { generateRandomString } from "../../../utils/stringHelper"; + +const DialogList = styled.div` + margin-top: 8px; + padding: 8px; + overflow: auto; + height: calc(100vh - 110px); +`; + +const DialogItem = styled.div` + padding: 8px; + border: 1px solid #d9d9d9; + border-radius: 8px; + cursor: pointer; + margin-bottom: 8px; + transition: border-color 0.3s linear; + &:hover { + border-color: #1890ff; + } + + // 当组件被选中时修改样式 + &.selected { + border-color: #1890ff; + } +`; + +export default function DesktopLayout() { + const id = new URLSearchParams(window.location.search).get('id') as string; + if (!id) { + return (
+ 请提供分享Id +
) + } + + /** + * 获取游客id + */ + let guestId = localStorage.getItem('ChatShare') + if (!guestId) { + guestId = generateRandomString(10) + localStorage.setItem('ChatShare', guestId) + } + const navigate = useNavigate(); + const [applications, setApplications] = useState([] as any[]); + const [application, setApplication] = useState(null as any); + const [dialogs, setDialogs] = useState([] as any[]); + const [createDialogVisible, setCreateDialogVisible] = useState(false); + const [dialog, setDialog] = useState({} as any); + const [history, setHistory] = useState([] as any[]); + const [value, setValue] = useState('' as string); + const [loading, setLoading] = useState(false); + const [input] = useState({ + page: 1, + pageSize: 5 + }); + + useEffect(() => { + loadingApplication(); + }, [id]) + + + async function loadingApplication() { + const app = await GetChatShareApplication(id as any); + setApplication(app) + } + + + async function loadingApplications() { + try { + const result = await GetChatApplicationsList(1, 1000); + setApplications(result.result); + if (result.total === 0) { + message.error('您还没有应用,请先创建应用'); + // 等待1秒后跳转 + setTimeout(() => { + navigate('/app'); + }, 1000); + return; + } + setApplication(result.result[0]); + + } catch (error) { + + } + } + + async function loadingDialogs() { + try { + + const result = (await GetChatShareDialog(id)) as any[]; + setDialogs(result); + if (result.length === 0) { + await AddChatDialog({ + name: '默认对话', + chatId: id, + description: '默认创建的对话', + applicationId: application.id, + type: 1 + }) + loadingDialogs(); + return; + } + setDialog(result[0]); + } catch (error) { + + } + } + + async function LoadingSession() { + try { + const result = await GetChatDialogHistory(dialog.id, input.page, input.pageSize); + + const history = result.result.map((item: any) => { + return { + content: item.content, + createAt: item.createAt, + extra: {}, + id: item.id, + meta: { + avatar: item.current ? "https://blog-simple.oss-cn-shenzhen.aliyuncs.com/Avatar.jpg" : "https://blog-simple.oss-cn-shenzhen.aliyuncs.com/chatgpt.png", + title: item.current ? "我" : "AI助手", + }, + role: item.current ? 'user' : 'assistant', + }; + }); + + setHistory(history); + + // 等待1秒后滚动到底部 + setTimeout(() => { + const chatlayout = document.getElementById('chat-layout'); + if (chatlayout) { + chatlayout.scrollTop = chatlayout.scrollHeight; + } + }, 1000); + } catch (error) { + + } + } + + async function AddChatDialog(data: any) { + try { + await CreateChatDialog(data) + } catch (error) { + message.error('创建失败'); + } + } + + useEffect(() => { + if (dialog) { + LoadingSession(); + } + }, [dialog, input]); + + useEffect(() => { + loadingApplications(); + loadingDialogs(); + }, []); + + function handleChange(value: any) { + console.log(`selected ${value}`); + } + + const control: ChatListProps | any = + { + showTitle: false, + } + + async function sendChat() { + const v = value; + setValue(''); + if(loading){ + return; + } + setLoading(true); + const chatlayout = document.getElementById('chat-layout'); + + history.push({ + content: v, + createAt: new Date().toISOString(), + extra: {}, + id: new Date().getTime(), + meta: { + avatar: "https://blog-simple.oss-cn-shenzhen.aliyuncs.com/Avatar.jpg", + title: "我", + }, + role: 'user', + }) + + setHistory([...history]); + + let chat = { + content: '', + createAt: new Date().toISOString(), + extra: {}, + id: new Date().getTime(), + meta: { + avatar: "https://blog-simple.oss-cn-shenzhen.aliyuncs.com/chatgpt.png", + title: "AI助手", + }, + role: 'assistant', + }; + + setHistory([...history, chat]); + + // 滚动到底部 + if (chatlayout) { + chatlayout.scrollTop = chatlayout.scrollHeight; + } + + const stream = await fetchRaw('/api/v1/ChatApplications/ChatShareCompletions', { + chatDialogId: dialog.id, + content: v, + chatId: application.id, + chatShareId: id, + }); + + for await (const c of stream) { + if (c) { + let content = c; + // 先匹配删除前缀 [ 和后缀 ] + if (content.startsWith('[') || content.startsWith(',')) { + // 删除第一个字符 + content = c.slice(1); + } + if (content.endsWith(']')) { + // 删除最后一个字符 + content = c.slice(0, c.length - 1); + } + if (content === '') { + return; + } + + if (content.startsWith(',') === true) { + content = content.slice(1); + } + + content = "[" + content + "]"; + console.log(content); + + var obj = JSON.parse(content) as any[]; + + obj.forEach((item) => { + chat.content += item.content; + }); + + setHistory([...history, chat]); + // 滚动到底部 + if (chatlayout) { + chatlayout.scrollTop = chatlayout.scrollHeight; + } + } + } + + await CreateChatDialogHistory({ + chatDialogId: dialog.id, + content: v, + current: true, + type: 0 + }) + + + await CreateChatDialogHistory({ + chatDialogId: dialog.id, + content: chat.content, + current: false, + type: 0 + }) + + + setLoading(false); + } + + + function deleteDialog(id: string) { + DeleteDialog(id) + .then(() => { + loadingDialogs(); + }) + } + + async function ActionsClick(e: any, item: any) { + if (e.key === 'del') { + await DeleteDialogHistory(item.id) + message.success('删除成功'); + + const index = history.findIndex((i) => i.id === item.id); + history.splice(index, 1); + setHistory([...history]); + + } else if (e.key === 'regenerate') { + message.error('暂时并未支持重置!'); + } + } + + return <> + + +
+ +
+ 请选择您的应用 +
+ { + setData({ + ...data, + name: e.target.value + }) + }} + placeholder="对话名称"/> +
+
+ { + setData({ + ...data, + description: e.target.value + }) + }} + placeholder="对话描述"/> +
+ + ) +} \ No newline at end of file diff --git a/web/src/pages/share-chat/page.tsx b/web/src/pages/share-chat/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8a5cbb0890fc2f05e65ae5072879623a6e475158 --- /dev/null +++ b/web/src/pages/share-chat/page.tsx @@ -0,0 +1,7 @@ +import DesktopPage from './(desktop)'; +import MobilePage from './(mobile)'; +import AdaptiveLayout from '../../layouts/adaptive-layout'; + +export default function Chat() { + return ; +} \ No newline at end of file diff --git a/web/src/pages/user/page.tsx b/web/src/pages/user/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..576c5832271587fd9da7a2b93bd419cd99f72c31 --- /dev/null +++ b/web/src/pages/user/page.tsx @@ -0,0 +1,5 @@ +export default function User(){ + return(
+ User +
) +} \ No newline at end of file diff --git a/web/src/pages/wiki-detail/(desktop)/index.tsx b/web/src/pages/wiki-detail/(desktop)/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3f8a3c4f0feeb0fba6e70af1d267a947bc732585 --- /dev/null +++ b/web/src/pages/wiki-detail/(desktop)/index.tsx @@ -0,0 +1,136 @@ +import { memo, useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { Button } from 'antd'; + +import styled from 'styled-components'; +import { GetWikis } from '../../../services/WikiService'; +import { Avatar } from '@lobehub/ui'; +import WikiData from '../features/WikiData'; +import UploadWikiFile from '../features/UploadWikiFile'; +import SearchWikiDetail from '../features/SearchWikiDetail'; +import { WikiDto } from '../../../models'; +import WikiInfo from '../features/WikiInfo'; +const LeftTabs = styled.div` + width: 190px; + min-width: 190px; + height: 100%; + /* 右边需要有一个分割线 */ + border-right: 1px solid #464545; + display: flex; + flex-direction: column; + +`; + + +export default memo(() => { + const { id } = useParams<{ id: string }>(); + if (id === undefined) return (
id is undefined
) + + const [wiki, setWiki] = useState({} as WikiDto); + + const [tabs, setTabs] = useState([] as any[]); + + const [tab, setTab] = useState() as any; + + useEffect(() => { + loadingWiki(); + }, [id]); + + async function loadingWiki() { + GetWikis(id as string) + .then((wiki) => { + setWiki(wiki); + }); + } + + useEffect(() => { + loadingTabs(); + }, [wiki]); + + function loadingTabs() { + const tabs = [{ + key: 1, + label: '数据集' + }, { + key: 2, + label: '搜索测试' + }, { + key: 3, + label: '配置' + }]; + + changeTab(tabs[0]); + + //强制刷新 + setTabs([...tabs]); + } + + function changeTab(key: any) { + // 如果key是数字 + if (typeof key === 'number') { + key = tabs.find(item => item.key === key); + } + + setTab(key); + } + + return ( + <> + +
+ +
{wiki.name}
+
+
+ {tabs.map((item, index) => { + return + })} +
+
+
+ { + tab?.key === 1 && changeTab(key)} id={id} /> + } + { + tab?.key === 2 && changeTab(key)} id={id} /> + } + { + tab?.key === 3 && + } + { + tab === 'upload' && changeTab(key)} /> + } +
+ + ); +}) \ No newline at end of file diff --git a/web/src/pages/wiki-detail/(mobile)/index.tsx b/web/src/pages/wiki-detail/(mobile)/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5cbb509d08ce56112530af98bb3d9f37828b7685 --- /dev/null +++ b/web/src/pages/wiki-detail/(mobile)/index.tsx @@ -0,0 +1,5 @@ +export default function WikiDetail() { + return (
+ as +
) +} \ No newline at end of file diff --git a/web/src/pages/wiki-detail/features/SearchWikiDetail.tsx b/web/src/pages/wiki-detail/features/SearchWikiDetail.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6705f35dd2b442ac4ca786143647d63195b8b29b --- /dev/null +++ b/web/src/pages/wiki-detail/features/SearchWikiDetail.tsx @@ -0,0 +1,144 @@ +import { useState } from "react"; +import styled from 'styled-components'; +import { Button } from 'antd'; +import { GetSearchVectorQuantity } from "../../../services/WikiService"; + + +const Textarea = styled.textarea` + width: 100%; + height: 300px; + resize: none; + border: none; + border-radius: 8px; + outline: none; + padding: 6px; + font-size: 16px; + line-height: 1.5; +`; + +const DataItem = styled.div` + padding: 16px; + border: 1px solid #d9d9d9; + border-radius: 8px; + cursor: pointer; + margin-bottom: 16px; + transition: border-color 0.3s linear; + &:hover { + // 边框颜色发生变化 + border-color: #1890ff; + } +`; + +interface ISearchWikiDetailProps { + id: string; + onChagePath(key: string): void; +} + + + +export default function SearchWikiDetail({ + id, +}: ISearchWikiDetailProps) { + + const [value, setValue] = useState('') + + async function SearchVectorQuantity() { + try { + if (value === '') return; + const result = await GetSearchVectorQuantity(id, value, 0.4) + setData(result.result) + setElapsedTime(result.elapsedTime) + } catch (error) { + + } + } + + const [data, setData] = useState([]); + const [elapsedTime, setElapsedTime] = useState(null); + + return ( + <> +
+
+
+ 搜索测试 +
+ + +
+
+ { + data.length === 0 &&
+ 暂无数据 +
+ } + { + elapsedTime !== null &&
+ 耗时: {elapsedTime}ms +
+ } +
+ { + data.map((item, index) => { + return +
+ {item.fileName} + + #(相似度{item.relevance.toFixed(4)}) + +
+
+ {item.content} +
+
+ }) + } +
+
+
+ + ) +} \ No newline at end of file diff --git a/web/src/pages/wiki-detail/features/UploadWikiFile.tsx b/web/src/pages/wiki-detail/features/UploadWikiFile.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ff49d822560e887a57797aae9e427ca1cb5eaa9b --- /dev/null +++ b/web/src/pages/wiki-detail/features/UploadWikiFile.tsx @@ -0,0 +1,265 @@ + +import { Button, Steps, Upload, UploadProps, Table, Progress, Radio, Input, message } from 'antd'; +import { useState } from 'react'; +import { InboxOutlined, CloseOutlined } from '@ant-design/icons'; +import styled from 'styled-components'; +import { bytesToSize } from '../../../utils/stringHelper'; +import { ProcessMode, TrainingPattern } from '../../../models/index.d'; +import { UploadFile as FileService } from '../../../services/StorageService'; +import { CreateWikiDetails } from '../../../services/WikiService'; + + +const FileItem = styled.div` + transition: border-color 0.3s linear; + border: 1px solid #d9d9d9; + border-radius: 8px; + padding: 10px; + margin-right: 10px; + display: flex; + cursor: pointer; + margin-bottom: 10px; + &:hover { + border-color: #1890ff; + transition: border-color 0.3s linear; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + } +`; + +const { Dragger } = Upload; + +interface IUploadWikiFileProps { + id: string; + onChagePath(key: any): void; +} + +export default function UploadWikiFile({ id, onChagePath }: IUploadWikiFileProps) { + const [current, setCurrent] = useState(0); + // const [uploading, setUploading] = useState(false); + const [fileList, setFileList] = useState([]); + const [processMode, setProcessMode] = useState(ProcessMode.Auto); + const [trainingPattern, setTrainingPattern] = useState(TrainingPattern.Subsection); + const [subsection, setSubsection] = useState(400); // 分段长度 + const props: UploadProps = { + name: 'file', + multiple: true, + showUploadList: false, + accept: '.md,.pdf,.docs,.txt,.json,.excel,.word,.html', + beforeUpload: (file: any) => { + fileList.push(file); + setFileList([...fileList]); + return false; + } + }; + + const columns = [ + { + title: '文件名', + dataIndex: 'fileName', + key: 'fileName', + }, + { + title: '文件上传进度', + dataIndex: 'progress', + key: 'progress', + render: (value: number) => { + return + } + }, + { + title: '数据上传进度', + dataIndex: 'dataProgress', + key: 'dataProgress', + render: (value: number) => { + return + } + }, + ]; + + function saveFile() { + fileList.forEach(async (file) => { + const fileItem = await FileService(file); + file.progress = 100; + + setFileList([...fileList]); + + await CreateWikiDetails({ + name: file.name, + wikiId: id, + fileId: fileItem.id, + filePath: fileItem.path, + subsection: subsection, + mode: processMode, + trainingPattern: trainingPattern + }) + + file.dataProgress = 100; + + setFileList([...fileList]); + + }); + message.success('上传成功'); + } + + return (<> +
+ +
+ + + + { + current === 0 && <> + +

+ +

+

点击或推动文件上传

+

+ 支持单个或批量上传,支持 .md .pdf .docs .txt .json .excel .word .html等格式, + 最多支持1000个文件。单文件最大支持100M。 +

+
+
+ + {fileList.length > 0 && fileList.map((item, index) => { + return + {item.name} + + {bytesToSize(item.size || 0)} + + + { + setFileList(fileList.filter((_, i) => i !== index)); + }} /> + + + })} +
+ + + } + { + current === 1 && <> +
+ { + const value = Number(v.target.value); + setTrainingPattern(value as TrainingPattern); + }} value={trainingPattern}> + 文本拆分 + QA问答拆分 + + { + trainingPattern === TrainingPattern.Subsection &&
+ 处理模式: + { + const value = Number(v.target.value); + setProcessMode(value as ProcessMode); + }} value={processMode}> + 自动 + 自定义 + + { + processMode === ProcessMode.Custom &&
+ 理想: + { + setSubsection(Number(e.target.value)); + }} + style={{ + width: 200 + }} /> +
+ } + +
+ } + { + trainingPattern === TrainingPattern.QA &&
+ 暂时不支持 +
+ } +
+
{ + return { + fileName: item.name, + progress: item.progress || 0, + dataProgress: item.dataProgress || 0 + } + })} columns={columns} /> + + + + } + ) +} \ No newline at end of file diff --git a/web/src/pages/wiki-detail/features/WikiData.tsx b/web/src/pages/wiki-detail/features/WikiData.tsx new file mode 100644 index 0000000000000000000000000000000000000000..670c93541781964b197b1314de04ad62070b0699 --- /dev/null +++ b/web/src/pages/wiki-detail/features/WikiData.tsx @@ -0,0 +1,161 @@ +import { Table, Button, Dropdown, MenuProps, message } from 'antd'; +import { useEffect, useState } from 'react'; +import { DeleteWikiDetails, GetWikiDetailsList } from '../../../services/WikiService'; +import WikiDetailFile from './WikiDetailFile'; + +interface IWikiDataProps { + id: string; + onChagePath(key: string): void; +} + +export default function WikiData({ id, onChagePath }: IWikiDataProps) { + + const columns = [ + { + title: '文件名', + dataIndex: 'fileName', + key: 'fileName', + }, + { + title: '索引数量', + dataIndex: 'dataCount', + key: 'dataCount', + }, + { + title: '数据类型', + dataIndex: 'type', + key: 'type', + }, + { + title: '数据状态', + key: 'stateName', + dataIndex: 'stateName', + }, + { + title: '创建时间', + key: 'creationTime', + dataIndex: 'creationTime', + }, + { + title: '操作', + key: 'action', + render: (_: any, item: any) => ( + <> + + + + ), + }, + ] + + const [data, setData] = useState([] as any[]); + const [visible, setVisible] = useState(false); + const [openItem, setOpenItem] = useState({} as any); + const [total, setTotal] = useState(0); + const [input, setInput] = useState({ + keyword: '', + page: 1, + pageSize: 10 + } as any); + + const items: MenuProps['items'] = [ + { + key: '1', + onClick: () => { + onChagePath('upload') + }, + label: ( + + 上传文件 + + ), + }, + { + key: '2', + label: ( + + 网页链接 + + ), + }, + { + key: '3', + label: ( + + 自定义文本 + + ), + }, + ]; + + + async function RemoveDeleteWikiDetails(id: string) { + try { + await DeleteWikiDetails(id); + message.success('删除成功'); + } catch (error) { + message.error('删除失败'); + } + } + + + function handleTableChange(page: number, pageSize: number) { + setInput({ + ...input, + page: page, + pageSize: pageSize, + }); + } + + async function loadingData() { + try { + const result = await GetWikiDetailsList(id, input.keyword, input.page, input.pageSize); + setData(result.result); + setTotal(result.total); + } catch (error) { + + } + } + + useEffect(() => { + loadingData(); + }, [id, input]); + + return (<> +
+ 文件列表 +
+ + + +
+
+
+ { + setVisible(false); + }} wikiDetail={openItem} visible={visible} /> + ) +} \ No newline at end of file diff --git a/web/src/pages/wiki-detail/features/WikiDetailFile.tsx b/web/src/pages/wiki-detail/features/WikiDetailFile.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5ab04a90c49c313c7604a0267e61b50e06b40d20 --- /dev/null +++ b/web/src/pages/wiki-detail/features/WikiDetailFile.tsx @@ -0,0 +1,96 @@ +import { Modal, SpotlightCard } from "@lobehub/ui"; +import { useEffect, useState } from "react"; +import { DelDetailsVector, GetWikiDetailVectorQuantity } from "../../../services/WikiService"; +import { WikiDetailVectorQuantityDto } from "../../../models"; +import { Button, message } from "antd"; + +import { Flexbox } from 'react-layout-kit'; + + + + +interface IWikiDetailProps { + wikiDetail: any; + visible: boolean; + onClose: () => void; +} + +export default function WikiDetailFile({ + wikiDetail, + visible, + onClose +}: IWikiDetailProps) { + const [wikiDetailInfo, setWikiDetailInfo] = useState([] as WikiDetailVectorQuantityDto[]); + const [taotal, setTotal] = useState(0); + const [input, setInput] = useState({ + page: 1, + pageSize: 10, + wikiId: wikiDetail.id + } as any); + + useEffect(() => { + if (visible) { + loadingWikiDetail(); + setInput({ + ...input, + wikiId: wikiDetail.id + }); + } + + }, [wikiDetail, visible]); + + function loadingWikiDetail() { + if (wikiDetail.id === undefined) return; + GetWikiDetailVectorQuantity(wikiDetail.id, input.page, input.pageSize) + .then((wikiDetail) => { + setWikiDetailInfo(wikiDetail.result); + setTotal(wikiDetail.total); + }); + } + + async function removeWikiDetail(id: string) { + try { + await DelDetailsVector(id) + message.success('删除成功'); + loadingWikiDetail(); + } catch (error) { + message.error('删除失败'); + } + } + + function renderItem(item: WikiDetailVectorQuantityDto) { + return + +
{item.document_Id} #({item.index})
+
+ {item.content} +
+
+ +
+ } + + return ( + +
+ {wikiDetail.fileName} + +
+ + + {taotal} +
+ ) +} diff --git a/web/src/pages/wiki-detail/features/WikiInfo.tsx b/web/src/pages/wiki-detail/features/WikiInfo.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a3f784158bd61345ba286c02e46a41257f338aaa --- /dev/null +++ b/web/src/pages/wiki-detail/features/WikiInfo.tsx @@ -0,0 +1,125 @@ +import styled from 'styled-components'; +import { WikiDto } from '../../../models'; +import { Select } from 'antd'; +import { useEffect, useState } from 'react'; +import { getModels } from '../../../store/Model'; +import { Button, message, Input } from 'antd'; +import { GetWikis, PutWikis } from '../../../services/WikiService'; + +const Container = styled.div` + display: grid; + padding: 20px; + /* 屏幕居中显示 */ + margin: auto; + width: 580px; + overflow: auto; + // 隐藏滚动条 + &::-webkit-scrollbar { + display: none; + } + scrollbar-width: none; +`; + +const ListItem = styled.div` + display: flex; + justify-content: space-between; + padding: 20px; + width: 100%; + align-items: center; +`; + +interface IWikiInfoProps { + id: string; +} + +export default function WikiInfo(props: IWikiInfoProps) { + const [model, setModel] = useState([] as any[]); + const [embeddingModel, setEmbeddingModel] = useState([] as any[]); + const [wikiInfo, setWikiInfo] = useState({} as WikiDto); + useEffect(() => { + getModels() + .then((models) => { + setModel(models.chatModel.map((item) => { + return { label: item.label, value: item.value } + })); + + setEmbeddingModel(models.embeddingModel.map((item) => { + return { label: item.label, value: item.value } + })); + }); + }, []); + + useEffect(() => { + loadingWiki(); + }, [props.id]); + + async function saveWiki() { + try { + await PutWikis(wikiInfo) + message.success('保存成功'); + } catch (error) { + message.error('保存失败'); + } + } + function loadingWiki() { + GetWikis(props.id as string) + .then((wiki) => { + setWikiInfo(wiki); + }); + } + return ( + + + 名称 + { + setWikiInfo({ + ...wikiInfo, + name: e.target.value + }); + }}> + + + + 对话模型 + { + setWikiInfo({ + ...wikiInfo, + embeddingModel: v + }); + }} + options={embeddingModel} + /> + + + + ) +} diff --git a/web/src/pages/wiki-detail/page.tsx b/web/src/pages/wiki-detail/page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..466915e9783797038ff4e2b7091c4570ef20ee1d --- /dev/null +++ b/web/src/pages/wiki-detail/page.tsx @@ -0,0 +1,7 @@ +import DesktopPage from './(desktop)'; +import MobilePage from './(mobile)'; +import AdaptiveLayout from '../../layouts/adaptive-layout'; + +export default function Login() { + return ; +} \ No newline at end of file diff --git a/web/src/pages/wiki/(desktop)/index.tsx b/web/src/pages/wiki/(desktop)/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fa6c00f3f1774bc19548d90d2d17470850dfc96e --- /dev/null +++ b/web/src/pages/wiki/(desktop)/index.tsx @@ -0,0 +1,31 @@ +import { memo, useState } from "react"; +import Header from "../features/Header"; +import { AppList } from "../features/WikiList"; +import { Layout } from "@lobehub/ui"; + +const DesktopLayout = memo(() => { + const [input, setInput] = useState({ + page: 1, + pageSize: 12 + }); + return ( +
+ { + setInput({ + ...input, + page: 1 + }); + }}/> + } + > + { + setInput(v); + }} input={input}/> + +
) +}); + +export default DesktopLayout; \ No newline at end of file diff --git a/web/src/pages/wiki/(mobile)/index.tsx b/web/src/pages/wiki/(mobile)/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8a5b0cb6f1be55194e17edce05b11a569e911817 --- /dev/null +++ b/web/src/pages/wiki/(mobile)/index.tsx @@ -0,0 +1,16 @@ +import { memo, useState } from "react"; +import { AppList } from "../features/WikiList"; + +const MobileLayout = memo(() => { + const [input, setInput] = useState({ + page: 1, + pageSize: 10 + }); + return (<> + { + setInput(v); + }} input={input}/> + ) +}); + +export default MobileLayout; \ No newline at end of file diff --git a/web/src/pages/wiki/features/CreateWiki.tsx b/web/src/pages/wiki/features/CreateWiki.tsx new file mode 100644 index 0000000000000000000000000000000000000000..eaef0fcf7281df1a6a63a1241ab911c843f8522a --- /dev/null +++ b/web/src/pages/wiki/features/CreateWiki.tsx @@ -0,0 +1,163 @@ +import { Modal } from "@lobehub/ui"; +import { Form, Input, Button, message, Select, Upload } from 'antd'; +import { useEffect, useState } from "react"; +import { getModels } from "../../../store/Model"; +import { CreateWikis } from "../../../services/WikiService"; +import { PlusOutlined } from '@ant-design/icons'; +import { UploadFile } from "../../../services/StorageService"; + +interface ICreateAppProps { + visible: boolean; + onClose: () => void; + onSuccess: () => void; +} + +type CreateAppType = { + name?: string; +}; + +export function CreateApp(props: ICreateAppProps) { + + const [model, setModel] = useState([] as any[]); + const [embeddingModel, setEmbeddingModel] = useState([] as any[]); + + const [previewVisible, setPreviewVisible] = useState(false); + const [previewImage, setPreviewImage] = useState(""); + const [previewTitle, setPreviewTitle] = useState(""); + const [fileList, setFileList] = useState([] as any[]); + + const handleCancel = () => setPreviewVisible(false); + + const handlePreview = async (file: any) => { + if (!file.url && !file.preview) { + file.preview = await getBase64(file.originFileObj); + } + + setPreviewImage(file.url || file.preview); + setPreviewVisible(true); + setPreviewTitle( + file.name || file.url.substring(file.url.lastIndexOf("/") + 1) + ); + }; + + const handleChange = ({ fileList }: any) => setFileList(fileList); + + const getBase64 = (file: any) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result); + reader.onerror = (error) => reject(error); + }); + }; + + + useEffect(() => { + getModels() + .then((models) => { + setModel(models.chatModel.map((item) => { + return { label: item.label, value: item.value } + })); + setEmbeddingModel(models.embeddingModel.map((item) => { + return { label: item.label, value: item.value } + })); + }); + + }, []); + + + async function onFinish(values: any) { + try { + if (fileList.length === 0) { + return message.error('请上传头像'); + } + const resultFile = await UploadFile(fileList[0].originFileObj) + values.icon = resultFile.path; + await CreateWikis(values); + message.success('创建成功'); + props.onSuccess(); + } catch (e) { + message.error('创建失败'); + } + } + + function onFinishFailed(errorInfo: any) { + console.log('Failed:', errorInfo); + } + + return ( + +
+ + + {fileList.length >= 1 ? null : ( +
+ +
上传
+
+ )} +
+ + 预览 + +
+ + + label="知识库名称" + name="name" + rules={[{ required: true, message: '请输入您的知识库名称' }]}> + + + + + + + + + + + +
+ ) +} \ No newline at end of file diff --git a/web/src/pages/wiki/features/Header.tsx b/web/src/pages/wiki/features/Header.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d730e7b6c4d82054fa473207cd460d6d10c146e4 --- /dev/null +++ b/web/src/pages/wiki/features/Header.tsx @@ -0,0 +1,23 @@ +import { Button } from 'antd'; + +import { Header } from '@lobehub/ui'; +import { CreateApp } from './CreateWiki'; +import { useState } from 'react'; + +interface IAppHeaderProps { + onSucess: () => void; +} + +export default function AppHeader(props:IAppHeaderProps) { + const [visible, setVisible] = useState(false); + const onClose = () => setVisible(false); + return ( + <> +
setVisible(true)}>新增} nav={'知识库管理'} /> + { + props.onSucess(); + onClose(); + })}/> + + ) +} \ No newline at end of file diff --git a/web/src/pages/wiki/features/WikiList.tsx b/web/src/pages/wiki/features/WikiList.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a3bdd5cb4e17d5937c81b3891bf942ab18959dab --- /dev/null +++ b/web/src/pages/wiki/features/WikiList.tsx @@ -0,0 +1,106 @@ +import { Avatar, GridShowcase, LogoThree, SpotlightCard } from '@lobehub/ui'; +import { useEffect, useState } from 'react'; +import { Flexbox } from 'react-layout-kit'; +import { message, Button, Pagination } from 'antd'; +import { WikiDto } from '../../../models'; +import { useNavigate } from 'react-router-dom'; +import { GetWikisList, DeleteWikis } from '../../../services/WikiService'; +import { DeleteOutlined } from '@ant-design/icons'; + + +interface IAppListProps { + input: { + page: number; + pageSize: number; + } + setInput: (input: any) => void; +} + +export function AppList(props: IAppListProps) { + const navigate = useNavigate(); + + const [input, setInput] = useState({ + keyword: '', + page: 1, + pageSize: 12 + }); + + const [data, setData] = useState([]); + const [total, setTotal] = useState(0); + + const render = (item: WikiDto) => ( + + + + { + openWikiDetail(item.id); + }}> +
{item.name}
+
+ QA模型: + {item.model} +
+
+