diff --git a/.gitignore b/.gitignore index 1dfa72ce..506e28e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.claude + # docker image build .dockerignore node_modules diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..a1e99d1e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,270 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## ⚠️ Important Rules (READ FIRST) + +**When working on this project, ALWAYS follow these rules:** + +1. **Frontend work MUST use `bun`** - Never use `npm` for `webapp/_webapp`. Use `bun install`, `bun run build`, etc. +2. **All frontend code is in `webapp/_webapp`** - For UI bugs, state management, React components, always look here first. +3. **API changes follow this workflow:** + - First: Edit `.proto` files in `proto/` directory + - Second: Run `make gen` to generate Go and TypeScript code + - Third: Implement the Go handler in `internal/api/` or `internal/services/` + - Never manually edit files in `pkg/gen/` or `webapp/_webapp/src/pkg/gen/` +4. **Office Add-in uses `npm` only** - `webapp/office` is the exception, use `npm` there for compatibility. +5. **Check CLAUDE.md before asking** - Most architecture questions are answered in this file. + +## Project Overview + +PaperDebugger is an AI-powered academic writing assistant with Chrome extension frontend and Go backend. It integrates with Overleaf to provide intelligent suggestions, chat assistance, and AI-powered paper review capabilities. + +**Tech Stack:** +- **Backend**: Go 1.24+, Gin (HTTP), gRPC (API), MongoDB +- **Frontend**: React 19, TypeScript, Vite, Zustand (state management) +- **Chrome Extension**: Built from `webapp/_webapp` +- **Office Add-in**: Separate npm project in `webapp/office` +- **API**: Protocol Buffers with Buf for code generation +- **AI**: OpenAI API integration with optional XtraMCP orchestration backend + +## Architecture + +### Backend Structure + +``` +cmd/main.go - Entry point for backend server +internal/ + ├── api/ - HTTP (Gin) and gRPC API handlers + │ ├── auth/ - Authentication endpoints (login, logout, token refresh) + │ ├── chat/ - Chat and conversation management + │ ├── comment/ - Comment system + │ ├── project/ - Overleaf project integration + │ └── user/ - User settings and prompts + ├── services/ - Business logic layer + │ ├── chat.go/chat_v2.go - Chat service implementations + │ ├── toolkit/ - AI tool calling framework + │ │ ├── client/ - OpenAI API client wrappers + │ │ ├── registry/ - Tool registration system + │ │ └── tools/ - Available tools (file_read, latex parsing, etc.) + ├── models/ - Domain models and database schemas + └── libs/ - Shared libraries (db, jwt, logger, tex processing) +proto/ - Protocol Buffer definitions (auth, chat, comment, project, user) +pkg/gen/ - Generated gRPC/Protobuf code (auto-generated, don't edit) +``` + +**Key architectural patterns:** +- Dependency injection via Google Wire (`wire.go`, `wire_gen.go`) +- Service layer pattern: API handlers → Services → Models → Database +- Tool calling system for AI function execution in `internal/services/toolkit/` +- Protocol Buffers for API contracts between frontend and backend + +### Frontend Structure + +``` +webapp/ + ├── _webapp/ - Main Chrome extension (uses bun/npm) + │ ├── src/ + │ │ ├── pkg/gen/ - Generated Protobuf client code + │ │ └── (React components, stores, etc.) + │ └── dist/ - Build output (load in Chrome) + ├── office/ - Office Add-in (npm only, Word integration) + │ └── src/paperdebugger/office.js - Built from _webapp + └── oauth-landing/ - OAuth callback page +``` + +**Frontend state management**: Zustand stores, React Query for API calls + +## Common Commands + +### Backend Development + +```bash +# Install development dependencies (protoc, buf, wire, etc.) +make deps + +# Generate Protocol Buffer code and Wire dependency injection +make gen + +# Build backend binary +make build + +# Run backend server (requires MongoDB on localhost:27017) +./dist/pd.exe +# Server starts on http://localhost:6060 + +# Format code (buf, go fmt, npm format in webapp) +make fmt + +# Lint code (buf, golangci-lint, npm lint in webapp) +make lint + +# Run tests with coverage +make test + +# View test coverage in browser +make test-view +``` + +**Environment setup**: Copy `.env.example` to `.env` and configure MongoDB URI, OpenAI API keys, etc. + +**MongoDB requirement**: Start MongoDB locally with `docker run -d --name mongodb -p 27017:27017 mongo:latest` + +### Frontend Development + +#### Chrome Extension (webapp/_webapp) + +**Package manager**: **MUST use `bun`** (preferred) or `npm` as fallback + +```bash +cd webapp/_webapp + +# Install dependencies (prefer bun) +bun install + +# Development mode (watch and rebuild on changes) +bun run dev + +# Development server (for testing chat UI in browser) +bun run dev:chat + +# Full production build (all components) +bun run build + +# Build individual components +bun run _build:default # Main extension UI +bun run _build:background # Background service worker +bun run _build:office # Office Add-in bundle +bun run _build:settings # Settings page +bun run _build:popup # Extension popup + +# Environment-specific builds +bun run build:local:chrome # Local development +bun run build:stg:chrome # Staging +bun run build:prd:chrome # Production + +# Lint and format +bun run lint +bun run format +``` + +**Build Office Add-in for Word:** +```bash +# From webapp/_webapp, build office.js bundle +PD_API_ENDPOINT="https://app.paperdebugger.com" npm run _build:office +# Outputs to: webapp/office/src/paperdebugger/office.js +``` + +**Loading the extension in Chrome:** +1. Run `npm run build` (or environment-specific build) +2. Open `chrome://extensions/` +3. Enable "Developer mode" +4. Click "Load unpacked" and select `webapp/_webapp/dist` + +#### Office Add-in (webapp/office) + +**Package manager**: Use `npm` only (Office Add-in compatibility requirement) + +```bash +cd webapp/office + +# Install dependencies (npm only!) +npm install + +# Start development server (watches office.js for changes) +npm run dev-server + +# Start Word and load the add-in +npm run start + +# Stop the add-in +npm run stop + +# Production build +npm run build + +# Validate manifest +npm run validate +``` + +**Development workflow**: +1. Build `office.js` from `webapp/_webapp` (see above) +2. Run `npm run dev-server` in `webapp/office` to watch for changes +3. Run `npm run start` to launch Word with the add-in loaded + +## Protocol Buffer Workflow + +**CRITICAL: This is the ONLY way to modify APIs. Follow this order strictly.** + +When adding or modifying API endpoints: + +1. **First**: Edit `.proto` files in `proto/` directory (define the gRPC service and messages) +2. **Second**: Run `make gen` to regenerate Go and TypeScript code + - Backend code appears in `pkg/gen/` + - Frontend code appears in `webapp/_webapp/src/pkg/gen/` +3. **Third**: Implement the Go handler in `internal/api/` or business logic in `internal/services/` +4. **Fourth**: Use the generated TypeScript client in frontend (`webapp/_webapp/src/`) +5. **Never manually edit generated files** - They will be overwritten by `make gen` + +**Example workflow for adding a new endpoint:** +```bash +# 1. Edit proto/chat/v2/chat.proto to add new RPC method +# 2. Generate code +make gen +# 3. Implement handler in internal/api/chat/handler.go +# 4. Use in frontend via generated client +``` + +## Testing + +```bash +# Run all tests with coverage +PD_MONGO_URI="mongodb://localhost:27017" go test -coverprofile=coverage.out ./cmd/... ./internal/... ./webapp/... + +# View coverage report in browser +go tool cover -html=coverage.out +``` + +## Key Concepts + +### Tool Calling System +The AI chat uses a tool calling framework in `internal/services/toolkit/`: +- Tools are registered in `registry/registry.go` +- Each tool implements the tool interface (e.g., `tools/files/file_read.go`) +- OpenAI function calling is wrapped in `toolkit/client/` +- Tool execution results are stored in MongoDB via `db/tool_call_record.go` + +### XtraMCP Integration (Optional) +XtraMCP is a closed-source MCP orchestration backend that provides: +- Research-mode agents with literature search +- AI-powered paper review and critique +- Domain-specific academic writing revisions + +Local development works without XtraMCP. The error `"ERROR [AI Client] Failed to initialize XtraMCP session"` is expected when self-hosting without it. + +### Chat System +Two implementations exist: +- `chat.go` - Original implementation +- `chat_v2.go` - Enhanced version with improved streaming + +Both use OpenAI's chat completion API with tool calling support. + +## Custom Endpoint Configuration + +Users can point the extension to a self-hosted backend: +1. Open extension settings +2. Click version number 5 times to enable "Developer Tools" +3. Enter backend URL in "Backend Endpoint" field +4. Refresh the page + +## Docker Deployment + +```bash +# Build Docker image +make image + +# Push to registry +make push +``` + +Images are tagged with branch name and commit hash to `ghcr.io/paperdebugger/sharelatex-paperdebugger`. diff --git a/internal/api/chat/create_conversation_message_stream.go b/internal/api/chat/create_conversation_message_stream.go index 3d6b2faa..4bec1fab 100644 --- a/internal/api/chat/create_conversation_message_stream.go +++ b/internal/api/chat/create_conversation_message_stream.go @@ -207,9 +207,13 @@ func (s *ChatServerV1) prepare(ctx context.Context, projectId string, conversati } var latexFullSource string + var projectInstructions string switch conversationType { case chatv1.ConversationType_CONVERSATION_TYPE_DEBUG: latexFullSource = "latex_full_source is not available in debug mode" + if project != nil { + projectInstructions = project.Instructions + } default: if project == nil || project.IsOutOfDate() { return ctx, nil, nil, shared.ErrProjectOutOfDate("project is out of date") @@ -219,6 +223,7 @@ func (s *ChatServerV1) prepare(ctx context.Context, projectId string, conversati if err != nil { return ctx, nil, nil, err } + projectInstructions = project.Instructions } var conversation *models.Conversation @@ -229,7 +234,7 @@ func (s *ChatServerV1) prepare(ctx context.Context, projectId string, conversati actor.ID, projectId, latexFullSource, - project.Instructions, + projectInstructions, userInstructions, userMessage, userSelectedText, diff --git a/internal/api/chat/create_conversation_message_stream_v2.go b/internal/api/chat/create_conversation_message_stream_v2.go index b82adf5d..51616eec 100644 --- a/internal/api/chat/create_conversation_message_stream_v2.go +++ b/internal/api/chat/create_conversation_message_stream_v2.go @@ -2,6 +2,8 @@ package chat import ( "context" + "time" + "paperdebugger/internal/api/mapper" "paperdebugger/internal/libs/contextutil" "paperdebugger/internal/libs/shared" @@ -137,7 +139,7 @@ func (s *ChatServerV2) createConversation( } // appendConversationMessage appends a message to the conversation and writes it to the database -// Returns the Conversation object +// Returns the Conversation object and the active branch func (s *ChatServerV2) appendConversationMessage( ctx context.Context, userId bson.ObjectID, @@ -146,52 +148,80 @@ func (s *ChatServerV2) appendConversationMessage( userSelectedText string, surrounding string, conversationType chatv2.ConversationType, -) (*models.Conversation, error) { + parentMessageId string, +) (*models.Conversation, *models.Branch, error) { objectID, err := bson.ObjectIDFromHex(conversationId) if err != nil { - return nil, err + return nil, nil, err } conversation, err := s.chatServiceV2.GetConversationV2(ctx, userId, objectID) if err != nil { - return nil, err + return nil, nil, err + } + + // Ensure branches are initialized (migrate legacy data if needed) + conversation.EnsureBranches() + + var activeBranch *models.Branch + + // Handle branching / edit mode + if parentMessageId != "" { + // Create a new branch for the edit + var err error + activeBranch, err = conversation.CreateNewBranch("", parentMessageId) + if err != nil { + return nil, nil, shared.ErrBadRequest(err) + } + } else { + // Normal append - use active (latest) branch + activeBranch = conversation.GetActiveBranch() + if activeBranch == nil { + // This shouldn't happen after EnsureBranches, but handle it + return nil, nil, shared.ErrBadRequest("No active branch found") + } } + // Now we get the branch, we can append the message to the branch. userMsg, userOaiMsg, err := s.buildUserMessage(ctx, userMessage, userSelectedText, surrounding, conversationType) if err != nil { - return nil, err + return nil, nil, err } bsonMsg, err := convertToBSONV2(userMsg) if err != nil { - return nil, err + return nil, nil, err } - conversation.InappChatHistory = append(conversation.InappChatHistory, bsonMsg) - conversation.OpenaiChatHistoryCompletion = append(conversation.OpenaiChatHistoryCompletion, userOaiMsg) + + // Append to the active branch + activeBranch.InappChatHistory = append(activeBranch.InappChatHistory, bsonMsg) + activeBranch.OpenaiChatHistoryCompletion = append(activeBranch.OpenaiChatHistoryCompletion, userOaiMsg) + activeBranch.UpdatedAt = bson.NewDateTimeFromTime(time.Now()) if err := s.chatServiceV2.UpdateConversationV2(conversation); err != nil { - return nil, err + return nil, nil, err } - return conversation, nil + return conversation, activeBranch, nil } // prepare creates a new conversation if conversationId is "", otherwise appends a message to the conversation // conversationType can be switched multiple times within a single conversation -func (s *ChatServerV2) prepare(ctx context.Context, projectId string, conversationId string, userMessage string, userSelectedText string, surrounding string, modelSlug string, conversationType chatv2.ConversationType) (context.Context, *models.Conversation, *models.Settings, error) { +// Returns: context, conversation, activeBranch, settings, error +func (s *ChatServerV2) prepare(ctx context.Context, projectId string, conversationId string, userMessage string, userSelectedText string, surrounding string, modelSlug string, conversationType chatv2.ConversationType, parentMessageId string) (context.Context, *models.Conversation, *models.Branch, *models.Settings, error) { actor, err := contextutil.GetActor(ctx) if err != nil { - return ctx, nil, nil, err + return ctx, nil, nil, nil, err } project, err := s.projectService.GetProject(ctx, actor.ID, projectId) if err != nil && err != mongo.ErrNoDocuments { - return ctx, nil, nil, err + return ctx, nil, nil, nil, err } userInstructions, err := s.userService.GetUserInstructions(ctx, actor.ID) if err != nil { - return ctx, nil, nil, err + return ctx, nil, nil, nil, err } var latexFullSource string @@ -199,22 +229,27 @@ func (s *ChatServerV2) prepare(ctx context.Context, projectId string, conversati switch conversationType { case chatv2.ConversationType_CONVERSATION_TYPE_DEBUG: latexFullSource = "latex_full_source is not available in debug mode" + if project != nil { + projectInstructions = project.Instructions + } default: if project == nil || project.IsOutOfDate() { - return ctx, nil, nil, shared.ErrProjectOutOfDate("project is out of date") + return ctx, nil, nil, nil, shared.ErrProjectOutOfDate("project is out of date") } latexFullSource, err = project.GetFullContent() if err != nil { - return ctx, nil, nil, err + return ctx, nil, nil, nil, err } projectInstructions = project.Instructions } var conversation *models.Conversation + var activeBranch *models.Branch if conversationId == "" { + // Create a new conversation conversation, err = s.createConversation( ctx, actor.ID, @@ -228,8 +263,15 @@ func (s *ChatServerV2) prepare(ctx context.Context, projectId string, conversati modelSlug, conversationType, ) + if err != nil { + return ctx, nil, nil, nil, err + } + // For new conversations, ensure branches and get the active one + conversation.EnsureBranches() + activeBranch = conversation.GetActiveBranch() } else { - conversation, err = s.appendConversationMessage( + // Append to an existing conversation + conversation, activeBranch, err = s.appendConversationMessage( ctx, actor.ID, conversationId, @@ -237,11 +279,11 @@ func (s *ChatServerV2) prepare(ctx context.Context, projectId string, conversati userSelectedText, surrounding, conversationType, + parentMessageId, ) - } - - if err != nil { - return ctx, nil, nil, err + if err != nil { + return ctx, nil, nil, nil, err + } } ctx = contextutil.SetProjectID(ctx, conversation.ProjectID) @@ -249,10 +291,10 @@ func (s *ChatServerV2) prepare(ctx context.Context, projectId string, conversati settings, err := s.userService.GetUserSettings(ctx, actor.ID) if err != nil { - return ctx, conversation, nil, err + return ctx, conversation, activeBranch, nil, err } - return ctx, conversation, settings, nil + return ctx, conversation, activeBranch, settings, nil } func (s *ChatServerV2) CreateConversationMessageStream( @@ -261,16 +303,28 @@ func (s *ChatServerV2) CreateConversationMessageStream( ) error { ctx := stream.Context() + // Inject Overleaf auth into context if provided + if req.OverleafAuth != nil { + ctx = contextutil.SetOverleafAuth(ctx, &contextutil.OverleafAuth{ + Session: req.OverleafAuth.GetSession(), + GCLB: req.OverleafAuth.GetGclb(), + ProjectID: req.OverleafAuth.GetProjectId(), + CSRFToken: req.OverleafAuth.GetCsrfToken(), + }) + } + modelSlug := req.GetModelSlug() - ctx, conversation, settings, err := s.prepare( + ctx, conversation, activeBranch, settings, err := s.prepare( ctx, req.GetProjectId(), req.GetConversationId(), req.GetUserMessage(), req.GetUserSelectedText(), + req.GetSurrounding(), modelSlug, req.GetConversationType(), + req.GetParentMessageId(), ) if err != nil { return s.sendStreamError(stream, err) @@ -281,12 +335,13 @@ func (s *ChatServerV2) CreateConversationMessageStream( APIKey: settings.OpenAIAPIKey, } - openaiChatHistory, inappChatHistory, err := s.aiClientV2.ChatCompletionStreamV2(ctx, stream, conversation.ID.Hex(), modelSlug, conversation.OpenaiChatHistoryCompletion, llmProvider) + // Use active branch's history for the LLM call + openaiChatHistory, inappChatHistory, err := s.aiClientV2.ChatCompletionStreamV2(ctx, stream, conversation.ID.Hex(), modelSlug, activeBranch.OpenaiChatHistoryCompletion, llmProvider) if err != nil { return s.sendStreamError(stream, err) } - // Append messages to the conversation + // Append messages to the active branch bsonMessages := make([]bson.M, len(inappChatHistory)) for i := range inappChatHistory { bsonMsg, err := convertToBSONV2(&inappChatHistory[i]) @@ -295,16 +350,17 @@ func (s *ChatServerV2) CreateConversationMessageStream( } bsonMessages[i] = bsonMsg } - conversation.InappChatHistory = append(conversation.InappChatHistory, bsonMessages...) - conversation.OpenaiChatHistoryCompletion = openaiChatHistory + activeBranch.InappChatHistory = append(activeBranch.InappChatHistory, bsonMessages...) + activeBranch.OpenaiChatHistoryCompletion = openaiChatHistory + activeBranch.UpdatedAt = bson.NewDateTimeFromTime(time.Now()) if err := s.chatServiceV2.UpdateConversationV2(conversation); err != nil { return s.sendStreamError(stream, err) } if conversation.Title == services.DefaultConversationTitle { go func() { - protoMessages := make([]*chatv2.Message, len(conversation.InappChatHistory)) - for i, bsonMsg := range conversation.InappChatHistory { + protoMessages := make([]*chatv2.Message, len(activeBranch.InappChatHistory)) + for i, bsonMsg := range activeBranch.InappChatHistory { protoMessages[i] = mapper.BSONToChatMessageV2(bsonMsg) } title, err := s.aiClientV2.GetConversationTitleV2(ctx, protoMessages, llmProvider) diff --git a/internal/api/chat/get_conversation_v2.go b/internal/api/chat/get_conversation_v2.go index 2d69920e..c014b962 100644 --- a/internal/api/chat/get_conversation_v2.go +++ b/internal/api/chat/get_conversation_v2.go @@ -5,6 +5,7 @@ import ( "paperdebugger/internal/api/mapper" "paperdebugger/internal/libs/contextutil" + "paperdebugger/internal/libs/shared" chatv2 "paperdebugger/pkg/gen/api/chat/v2" "go.mongodb.org/mongo-driver/v2/bson" @@ -29,7 +30,25 @@ func (s *ChatServerV2) GetConversation( return nil, err } + // Migrate legacy data to branch structure if needed + // Persist immediately so branch IDs remain stable across API calls + if conversation.EnsureBranches() { + if err := s.chatServiceV2.UpdateConversationV2(conversation); err != nil { + return nil, err + } + } + + // Use specified branch_id if provided, otherwise use active branch + branchID := req.GetBranchId() + + // Validate that the provided branchId exists in the conversation + if branchID != "" { + if conversation.GetBranchByID(branchID) == nil { + return nil, shared.ErrBadRequest("branch_id not found in conversation") + } + } + return &chatv2.GetConversationResponse{ - Conversation: mapper.MapModelConversationToProtoV2(conversation), + Conversation: mapper.MapModelConversationToProtoV2WithBranch(conversation, branchID), }, nil } diff --git a/internal/api/chat/list_conversations_v2.go b/internal/api/chat/list_conversations_v2.go index 2b6fbf2e..87df8f15 100644 --- a/internal/api/chat/list_conversations_v2.go +++ b/internal/api/chat/list_conversations_v2.go @@ -25,6 +25,18 @@ func (s *ChatServerV2) ListConversations( return nil, err } + // Migrate legacy data to branch structure if needed + // Persist immediately so branch IDs remain stable across API calls + for _, conversation := range conversations { + if conversation.EnsureBranches() { + // Persist migration asynchronously to avoid blocking the response + // Errors are logged but don't fail the request + go func(c *models.Conversation) { + _ = s.chatServiceV2.UpdateConversationV2(c) + }(conversation) + } + } + return &chatv2.ListConversationsResponse{ Conversations: lo.Map(conversations, func(conversation *models.Conversation, _ int) *chatv2.Conversation { return mapper.MapModelConversationToProtoV2(conversation) diff --git a/internal/api/chat/list_supported_models_v2.go b/internal/api/chat/list_supported_models_v2.go index 7c37a7bd..dfd9cb05 100644 --- a/internal/api/chat/list_supported_models_v2.go +++ b/internal/api/chat/list_supported_models_v2.go @@ -10,6 +10,173 @@ import ( "github.com/openai/openai-go/v3" ) +// modelConfig holds model configuration including whether it requires user's own API key +type modelConfig struct { + name string + slugOpenRouter string // Slug for OpenRouter API (used when user doesn't provide own key) + slugOpenAI string // Slug for OpenAI API (used when user provides own key) + totalContext int64 + maxOutput int64 + inputPrice int64 + outputPrice int64 + requireOwnKey bool // If true, this model requires user to provide their own API key +} + +// allModels defines all available models in the system +var allModels = []modelConfig{ + // Free/default models (requireOwnKey = false) + { + name: "GPT-5.1", + slugOpenRouter: "openai/gpt-5.1", + slugOpenAI: openai.ChatModelGPT5_1, + totalContext: 400000, + maxOutput: 128000, + inputPrice: 125, // $1.25 + outputPrice: 1000, // $10.00 + requireOwnKey: false, + }, + { + name: "GPT-5.2", + slugOpenRouter: "openai/gpt-5.2", + slugOpenAI: openai.ChatModelGPT5_2, + totalContext: 400000, + maxOutput: 128000, + inputPrice: 175, // $1.75 + outputPrice: 1400, // $14.00 + requireOwnKey: true, + }, + { + name: "GPT-5 Mini", + slugOpenRouter: "openai/gpt-5-mini", + slugOpenAI: openai.ChatModelGPT5Mini, + totalContext: 400000, + maxOutput: 128000, + inputPrice: 25, + outputPrice: 200, + requireOwnKey: false, + }, + { + name: "GPT-5 Nano", + slugOpenRouter: "openai/gpt-5-nano", + slugOpenAI: openai.ChatModelGPT5Nano, + totalContext: 400000, + maxOutput: 128000, + inputPrice: 5, // $0.20 + outputPrice: 40, // $0.80 + requireOwnKey: false, + }, + { + name: "GPT-4.1", + slugOpenRouter: "openai/gpt-4.1", + slugOpenAI: openai.ChatModelGPT4_1, + totalContext: 1050000, + maxOutput: 32800, + inputPrice: 200, // $2.00 + outputPrice: 800, + requireOwnKey: false, + }, + { + name: "GPT-4.1-mini", + slugOpenRouter: "openai/gpt-4.1-mini", + slugOpenAI: openai.ChatModelGPT4_1Mini, + totalContext: 128000, + maxOutput: 16400, + inputPrice: 15, + outputPrice: 60, + requireOwnKey: false, + }, + { + name: "GPT-4o", + slugOpenRouter: "openai/gpt-4o", + slugOpenAI: openai.ChatModelGPT4o, + totalContext: 128000, + maxOutput: 16400, + inputPrice: 250, + outputPrice: 1000, + requireOwnKey: true, + }, + { + name: "Qwen Plus (balanced)", + slugOpenRouter: "qwen/qwen-plus", + slugOpenAI: "qwen-plus", // OpenAI doesn't support Qwen, use OpenRouter slug + totalContext: 131100, + maxOutput: 8200, + inputPrice: 40, + outputPrice: 120, + requireOwnKey: false, + }, + { + name: "Qwen Turbo (fast)", + slugOpenRouter: "qwen/qwen-turbo", + slugOpenAI: "", // OpenAI doesn't support Qwen, use OpenRouter slug + totalContext: 1000000, + maxOutput: 8200, + inputPrice: 5, + outputPrice: 20, + requireOwnKey: false, + }, + { + name: "Gemini 2.5 Flash (fast)", + slugOpenRouter: "google/gemini-2.5-flash", + slugOpenAI: "", // OpenAI doesn't support Gemini, use OpenRouter slug + totalContext: 1050000, + maxOutput: 65500, + inputPrice: 30, + outputPrice: 250, + requireOwnKey: false, + }, + { + name: "Gemini 3 Flash Preview", + slugOpenRouter: "google/gemini-3-flash-preview", + slugOpenAI: "", // OpenAI doesn't support Gemini, use OpenRouter slug + totalContext: 1050000, + maxOutput: 65500, + inputPrice: 50, + outputPrice: 300, + requireOwnKey: false, + }, + { + name: "o1 Mini", + slugOpenRouter: "openai/o1-mini", + slugOpenAI: openai.ChatModelO1Mini, + totalContext: 128000, + maxOutput: 65536, + inputPrice: 300, // $3.00 + outputPrice: 1200, // $12.00 + requireOwnKey: true, + }, + { + name: "o3", + slugOpenRouter: "openai/o3", + slugOpenAI: openai.ChatModelO3, + totalContext: 200000, + maxOutput: 100000, + inputPrice: 200, + outputPrice: 800, + requireOwnKey: true, + }, + { + name: "o3 Mini", + slugOpenRouter: "openai/o3-mini", + slugOpenAI: openai.ChatModelO3Mini, + totalContext: 200000, + maxOutput: 100000, + inputPrice: 110, + outputPrice: 440, + requireOwnKey: true, + }, + { + name: "o4 Mini", + slugOpenRouter: "openai/o4-mini", + slugOpenAI: openai.ChatModelO4Mini, + totalContext: 128000, + maxOutput: 65536, + inputPrice: 110, + outputPrice: 440, + requireOwnKey: true, + }, +} + func (s *ChatServerV2) ListSupportedModels( ctx context.Context, req *chatv2.ListSupportedModelsRequest, @@ -24,89 +191,40 @@ func (s *ChatServerV2) ListSupportedModels( return nil, err } + hasOwnAPIKey := strings.TrimSpace(settings.OpenAIAPIKey) != "" + var models []*chatv2.SupportedModel - if strings.TrimSpace(settings.OpenAIAPIKey) == "" { - models = []*chatv2.SupportedModel{ - { - Name: "GPT-4.1", - Slug: "openai/gpt-4.1", - TotalContext: 1050000, - MaxOutput: 32800, - InputPrice: 200, - OutputPrice: 800, - }, - { - Name: "GPT-4.1-mini", - Slug: "openai/gpt-4.1-mini", - TotalContext: 128000, - MaxOutput: 16400, - InputPrice: 15, - OutputPrice: 60, - }, - { - Name: "Qwen Plus (balanced)", - Slug: "qwen/qwen-plus", - TotalContext: 131100, - MaxOutput: 8200, - InputPrice: 40, - OutputPrice: 120, - }, - { - Name: "Qwen Turbo (fast)", - Slug: "qwen/qwen-turbo", - TotalContext: 1000000, - MaxOutput: 8200, - InputPrice: 5, - OutputPrice: 20, - }, - { - Name: "Gemini 2.5 Flash (fast)", - Slug: "google/gemini-2.5-flash", - TotalContext: 1050000, - MaxOutput: 65500, - InputPrice: 30, - OutputPrice: 250, - }, - { - Name: "Gemini 3 Flash Preview", - Slug: "google/gemini-3-flash-preview", - TotalContext: 1050000, - MaxOutput: 65500, - InputPrice: 50, - OutputPrice: 300, - }, + for _, config := range allModels { + // Choose the appropriate slug based on whether user has their own API key + slug := config.slugOpenRouter + if hasOwnAPIKey { + slug = config.slugOpenAI } - } else { - models = []*chatv2.SupportedModel{ - { - Name: "GPT-4.1", - Slug: openai.ChatModelGPT4_1, - TotalContext: 1050000, - MaxOutput: 32800, - InputPrice: 200, - OutputPrice: 800, - }, - { - Name: "GPT-4o", - Slug: openai.ChatModelGPT4o, - TotalContext: 128000, - MaxOutput: 16400, - InputPrice: 250, - OutputPrice: 1000, - }, - { - Name: "GPT-4.1-mini", - Slug: openai.ChatModelGPT4_1Mini, - TotalContext: 128000, - MaxOutput: 16400, - InputPrice: 15, - OutputPrice: 60, - }, - // TODO: add user custom models + + model := &chatv2.SupportedModel{ + Name: config.name, + Slug: slug, + TotalContext: config.totalContext, + MaxOutput: config.maxOutput, + InputPrice: config.inputPrice, + OutputPrice: config.outputPrice, } + + // If model requires own key but user hasn't provided one, mark as disabled + if config.requireOwnKey && !hasOwnAPIKey { + model.Disabled = true + model.DisabledReason = stringPtr("Requires your own OpenAI API key. Configure it in Settings.") + } + + models = append(models, model) } return &chatv2.ListSupportedModelsResponse{ Models: models, }, nil } + +// stringPtr returns a pointer to the given string +func stringPtr(s string) *string { + return &s +} diff --git a/internal/api/grpc.go b/internal/api/grpc.go index ed9dc2b0..7be4b03f 100644 --- a/internal/api/grpc.go +++ b/internal/api/grpc.go @@ -15,6 +15,7 @@ import ( chatv2 "paperdebugger/pkg/gen/api/chat/v2" commentv1 "paperdebugger/pkg/gen/api/comment/v1" projectv1 "paperdebugger/pkg/gen/api/project/v1" + projectv2 "paperdebugger/pkg/gen/api/project/v2" userv1 "paperdebugger/pkg/gen/api/user/v1" // "github.com/grpc-ecosystem/go-grpc-middleware" @@ -105,6 +106,7 @@ func NewGrpcServer( chatServerV2 chatv2.ChatServiceServer, userServer userv1.UserServiceServer, projectServer projectv1.ProjectServiceServer, + projectServerV2 projectv2.ProjectServiceServer, commentServer commentv1.CommentServiceServer, ) *GrpcServer { grpcServer := &GrpcServer{} @@ -113,6 +115,13 @@ func NewGrpcServer( grpcServer.Server = grpc.NewServer( grpc.UnaryInterceptor(grpcServer.grpcUnaryAuthInterceptor), grpc.StreamInterceptor(grpcServer.grpcStreamAuthInterceptor), + // Disable write buffer to enable immediate streaming + grpc.WriteBufferSize(0), + // Disable read buffer size to reduce latency + grpc.ReadBufferSize(0), + // Set initial window size for faster streaming + grpc.InitialWindowSize(65536), + grpc.InitialConnWindowSize(65536), ) authv1.RegisterAuthServiceServer(grpcServer.Server, authServer) @@ -120,6 +129,7 @@ func NewGrpcServer( chatv2.RegisterChatServiceServer(grpcServer.Server, chatServerV2) userv1.RegisterUserServiceServer(grpcServer.Server, userServer) projectv1.RegisterProjectServiceServer(grpcServer.Server, projectServer) + projectv2.RegisterProjectServiceServer(grpcServer.Server, projectServerV2) commentv1.RegisterCommentServiceServer(grpcServer.Server, commentServer) return grpcServer } diff --git a/internal/api/mapper/conversation_v2.go b/internal/api/mapper/conversation_v2.go index 23c10fae..1216c6f7 100644 --- a/internal/api/mapper/conversation_v2.go +++ b/internal/api/mapper/conversation_v2.go @@ -22,9 +22,45 @@ func BSONToChatMessageV2(msg bson.M) *chatv2.Message { return m } +// MapModelConversationToProtoV2 converts a conversation model to proto. +// Uses the active branch by default, or the specified branchID if provided. func MapModelConversationToProtoV2(conversation *models.Conversation) *chatv2.Conversation { + return MapModelConversationToProtoV2WithBranch(conversation, "") +} + +// MapModelConversationToProtoV2WithBranch converts a conversation model to proto +// with explicit branch selection. If branchID is empty, uses the active branch. +func MapModelConversationToProtoV2WithBranch(conversation *models.Conversation, branchID string) *chatv2.Conversation { + // Ensure branches are initialized (migrate legacy data if needed) + conversation.EnsureBranches() + + // Determine which branch to use + var selectedBranch *models.Branch + var currentBranchID string + var currentBranchIndex int32 + + if branchID != "" { + selectedBranch = conversation.GetBranchByID(branchID) + } + if selectedBranch == nil { + selectedBranch = conversation.GetActiveBranch() + } + + // Get messages from the selected branch or use legacy fallback + var inappHistory []bson.M + if selectedBranch != nil { + inappHistory = selectedBranch.InappChatHistory + currentBranchID = selectedBranch.ID + currentBranchIndex = int32(conversation.GetBranchIndex(selectedBranch.ID)) + } else { + // Fallback to legacy fields (should not happen after EnsureBranches) + inappHistory = conversation.InappChatHistory + currentBranchID = "" + currentBranchIndex = 1 + } + // Convert BSON messages back to protobuf messages - filteredMessages := lo.Map(conversation.InappChatHistory, func(msg bson.M, _ int) *chatv2.Message { + filteredMessages := lo.Map(inappHistory, func(msg bson.M, _ int) *chatv2.Message { return BSONToChatMessageV2(msg) }) @@ -37,10 +73,23 @@ func MapModelConversationToProtoV2(conversation *models.Conversation) *chatv2.Co modelSlug = models.SlugFromLanguageModel(models.LanguageModel(conversation.LanguageModel)) } + // Build branch info list + branches := lo.Map(conversation.Branches, func(b models.Branch, _ int) *chatv2.BranchInfo { + return &chatv2.BranchInfo{ + Id: b.ID, + CreatedAt: int64(b.CreatedAt), + UpdatedAt: int64(b.UpdatedAt), + } + }) + return &chatv2.Conversation{ - Id: conversation.ID.Hex(), - Title: conversation.Title, - ModelSlug: modelSlug, - Messages: filteredMessages, + Id: conversation.ID.Hex(), + Title: conversation.Title, + ModelSlug: modelSlug, + Messages: filteredMessages, + CurrentBranchId: currentBranchID, + Branches: branches, + CurrentBranchIndex: currentBranchIndex, + TotalBranches: int32(len(conversation.Branches)), } } diff --git a/internal/api/mapper/project.go b/internal/api/mapper/project.go index a2e3c211..206ee52c 100644 --- a/internal/api/mapper/project.go +++ b/internal/api/mapper/project.go @@ -3,10 +3,12 @@ package mapper import ( "paperdebugger/internal/models" projectv1 "paperdebugger/pkg/gen/api/project/v1" + projectv2 "paperdebugger/pkg/gen/api/project/v2" "google.golang.org/protobuf/types/known/timestamppb" ) +// V1 Mappers func MapModelProjectDocToProto(doc models.ProjectDoc) *projectv1.ProjectDoc { return &projectv1.ProjectDoc{ Id: doc.ID, @@ -35,3 +37,104 @@ func MapModelProjectToProto(project *models.Project) *projectv1.Project { // Do not map docs here, user should get docs from the "websocket sync" } } + +// V2 Mappers +func MapModelProjectDocToProtoV2(doc models.ProjectDoc) *projectv2.ProjectDoc { + return &projectv2.ProjectDoc{ + Id: doc.ID, + Version: int32(doc.Version), + Filename: extractFilename(doc.Filepath), + Filepath: doc.Filepath, + Lines: doc.Lines, + } +} + +func MapProtoProjectDocToModelV2(doc *projectv2.ProjectDoc) models.ProjectDoc { + return models.ProjectDoc{ + ID: doc.Id, + Version: int(doc.Version), + Filepath: doc.Filepath, + Lines: doc.Lines, + } +} + +func MapModelProjectFolderToProto(folder *models.ProjectFolder) *projectv2.ProjectFolder { + if folder == nil { + return nil + } + + protoDocs := make([]*projectv2.ProjectDoc, len(folder.Docs)) + for i, doc := range folder.Docs { + protoDocs[i] = MapModelProjectDocToProtoV2(doc) + } + + protoFolders := make([]*projectv2.ProjectFolder, len(folder.Folders)) + for i := range folder.Folders { + protoFolders[i] = MapModelProjectFolderToProto(&folder.Folders[i]) + } + + return &projectv2.ProjectFolder{ + Id: folder.ID, + Name: folder.Name, + Docs: protoDocs, + Folders: protoFolders, + } +} + +func MapProtoProjectFolderToModel(folder *projectv2.ProjectFolder) *models.ProjectFolder { + if folder == nil { + return nil + } + + modelDocs := make([]models.ProjectDoc, len(folder.Docs)) + for i, doc := range folder.Docs { + modelDocs[i] = MapProtoProjectDocToModelV2(doc) + } + + modelFolders := make([]models.ProjectFolder, len(folder.Folders)) + for i, subFolder := range folder.Folders { + if mapped := MapProtoProjectFolderToModel(subFolder); mapped != nil { + modelFolders[i] = *mapped + } + } + + return &models.ProjectFolder{ + ID: folder.Id, + Name: folder.Name, + Docs: modelDocs, + Folders: modelFolders, + } +} + +func MapModelProjectToProtoV2(project *models.ProjectV2) *projectv2.Project { + return &projectv2.Project{ + Id: project.ProjectID, + CreatedAt: timestamppb.New(project.CreatedAt.Time()), + UpdatedAt: timestamppb.New(project.UpdatedAt.Time()), + Name: project.Name, + RootDocId: project.RootDocID, + RootFolder: MapModelProjectFolderToProto(project.RootFolder), + Instructions: project.Instructions, + } +} + +func MapProtoProjectToModelV2(proto *projectv2.Project) *models.ProjectV2 { + return &models.ProjectV2{ + ProjectID: proto.Id, + Name: proto.Name, + RootDocID: proto.RootDocId, + RootFolder: MapProtoProjectFolderToModel(proto.RootFolder), + Instructions: proto.Instructions, + } +} + +// Helper function to extract filename from filepath +func extractFilename(filepath string) string { + // Extract filename from filepath (e.g., "sections/intro.tex" -> "intro.tex") + for i := len(filepath) - 1; i >= 0; i-- { + if filepath[i] == '/' || filepath[i] == '\\' { + return filepath[i+1:] + } + } + return filepath +} diff --git a/internal/api/project/v2/get_project.go b/internal/api/project/v2/get_project.go new file mode 100644 index 00000000..50072522 --- /dev/null +++ b/internal/api/project/v2/get_project.go @@ -0,0 +1,33 @@ +package v2 + +import ( + "context" + + "paperdebugger/internal/api/mapper" + "paperdebugger/internal/libs/contextutil" + "paperdebugger/internal/libs/shared" + projectv2 "paperdebugger/pkg/gen/api/project/v2" +) + +func (s *ProjectServerV2) GetProject( + ctx context.Context, + req *projectv2.GetProjectRequest, +) (*projectv2.GetProjectResponse, error) { + actor, err := contextutil.GetActor(ctx) + if err != nil { + return nil, err + } + + if req.GetProjectId() == "" { + return nil, shared.ErrBadRequest("project_id is required") + } + + project, err := s.projectService.GetProjectV2(ctx, actor.ID, req.GetProjectId()) + if err != nil { + return nil, err + } + + return &projectv2.GetProjectResponse{ + Project: mapper.MapModelProjectToProtoV2(project), + }, nil +} diff --git a/internal/api/project/v2/server.go b/internal/api/project/v2/server.go new file mode 100644 index 00000000..44f7ae67 --- /dev/null +++ b/internal/api/project/v2/server.go @@ -0,0 +1,27 @@ +package v2 + +import ( + "paperdebugger/internal/libs/cfg" + "paperdebugger/internal/libs/logger" + "paperdebugger/internal/services" + projectv2 "paperdebugger/pkg/gen/api/project/v2" +) + +type ProjectServerV2 struct { + projectv2.UnimplementedProjectServiceServer + projectService *services.ProjectService + logger *logger.Logger + cfg *cfg.Cfg +} + +func NewProjectServerV2( + projectService *services.ProjectService, + logger *logger.Logger, + cfg *cfg.Cfg, +) projectv2.ProjectServiceServer { + return &ProjectServerV2{ + projectService: projectService, + logger: logger, + cfg: cfg, + } +} diff --git a/internal/api/project/v2/upsert_project.go b/internal/api/project/v2/upsert_project.go new file mode 100644 index 00000000..70f8f307 --- /dev/null +++ b/internal/api/project/v2/upsert_project.go @@ -0,0 +1,45 @@ +package v2 + +import ( + "context" + + "paperdebugger/internal/api/mapper" + "paperdebugger/internal/libs/contextutil" + "paperdebugger/internal/libs/shared" + "paperdebugger/internal/models" + projectv2 "paperdebugger/pkg/gen/api/project/v2" +) + +func (s *ProjectServerV2) UpsertProject( + ctx context.Context, + req *projectv2.UpsertProjectRequest, +) (*projectv2.UpsertProjectResponse, error) { + actor, err := contextutil.GetActor(ctx) + if err != nil { + return nil, err + } + + if req.GetProjectId() == "" { + return nil, shared.ErrBadRequest("project_id is required") + } + if req.GetName() == "" { + return nil, shared.ErrBadRequest("name is required") + } + + project := &models.ProjectV2{ + ProjectID: req.GetProjectId(), + Name: req.GetName(), + RootDocID: req.GetRootDocId(), + RootFolder: mapper.MapProtoProjectFolderToModel(req.GetRootFolder()), + Instructions: req.GetInstructions(), + } + + project, err = s.projectService.UpsertProjectV2(ctx, actor.ID, req.GetProjectId(), project) + if err != nil { + return nil, err + } + + return &projectv2.UpsertProjectResponse{ + Project: mapper.MapModelProjectToProtoV2(project), + }, nil +} diff --git a/internal/api/server.go b/internal/api/server.go index b093c767..e2f99892 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -16,6 +16,7 @@ import ( chatv2 "paperdebugger/pkg/gen/api/chat/v2" commentv1 "paperdebugger/pkg/gen/api/comment/v1" projectv1 "paperdebugger/pkg/gen/api/project/v1" + projectv2 "paperdebugger/pkg/gen/api/project/v2" sharedv1 "paperdebugger/pkg/gen/api/shared/v1" userv1 "paperdebugger/pkg/gen/api/user/v1" @@ -97,7 +98,12 @@ func (s *Server) Run(addr string) { } err = projectv1.RegisterProjectServiceHandler(context.Background(), mux, client) if err != nil { - s.logger.Fatalf("failed to register project service grpc gateway: %v", err) + s.logger.Fatalf("failed to register project v1 service grpc gateway: %v", err) + return + } + err = projectv2.RegisterProjectServiceHandler(context.Background(), mux, client) + if err != nil { + s.logger.Fatalf("failed to register project v2 service grpc gateway: %v", err) return } err = commentv1.RegisterCommentServiceHandler(context.Background(), mux, client) diff --git a/internal/libs/contextutil/contextutil.go b/internal/libs/contextutil/contextutil.go index 8ba56dce..c0586832 100644 --- a/internal/libs/contextutil/contextutil.go +++ b/internal/libs/contextutil/contextutil.go @@ -67,3 +67,27 @@ func GetConversationID(ctx context.Context) (string, error) { } return v, nil } + +// OverleafAuth contains authentication information for Overleaf API calls. +type OverleafAuth struct { + Session string // overleaf_session2 cookie + GCLB string // GCLB cookie (Google Cloud Load Balancer) + ProjectID string // Current Overleaf project ID + CSRFToken string // CSRF token for API requests +} + +const overleafAuthKey = "overleafAuth" + +func SetOverleafAuth(ctx context.Context, auth *OverleafAuth) context.Context { + return Set(ctx, overleafAuthKey, auth) +} + +// GetOverleafAuth returns the Overleaf authentication from the context. +// Returns nil and error if not found - callers should check if Overleaf auth is required. +func GetOverleafAuth(ctx context.Context) (*OverleafAuth, error) { + v, ok := Get[*OverleafAuth](ctx, overleafAuthKey) + if !ok { + return nil, shared.ErrBadRequest("overleaf auth not found in context") + } + return v, nil +} diff --git a/internal/libs/overleaf/client.go b/internal/libs/overleaf/client.go new file mode 100644 index 00000000..88ff2f93 --- /dev/null +++ b/internal/libs/overleaf/client.go @@ -0,0 +1,216 @@ +package overleaf + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "paperdebugger/internal/libs/contextutil" +) + +// Client is an HTTP client for Overleaf API calls. +type Client struct { + httpClient *http.Client + baseURL string +} + +// NewClient creates a new Overleaf API client. +func NewClient() *Client { + return &Client{ + httpClient: &http.Client{Timeout: 30 * time.Second}, + baseURL: "https://www.overleaf.com", + } +} + +// NewClientWithBaseURL creates a new Overleaf API client with a custom base URL. +// Useful for testing or self-hosted Overleaf instances. +func NewClientWithBaseURL(baseURL string) *Client { + return &Client{ + httpClient: &http.Client{Timeout: 30 * time.Second}, + baseURL: baseURL, + } +} + +// CreateDocRequest represents the request body for creating a document. +type CreateDocRequest struct { + ParentFolderID string `json:"parent_folder_id"` + Name string `json:"name"` +} + +// CreateDocResponse represents the response from creating a document. +type CreateDocResponse struct { + ID string `json:"_id"` + Name string `json:"name"` +} + +// CreateFolderRequest represents the request body for creating a folder. +type CreateFolderRequest struct { + ParentFolderID string `json:"parent_folder_id"` + Name string `json:"name"` +} + +// CreateFolderResponse represents the response from creating a folder. +type CreateFolderResponse struct { + ID string `json:"_id"` + Name string `json:"name"` +} + +// DeleteDocRequest represents the request for deleting a document. +type DeleteDocRequest struct { + DocID string +} + +// DeleteFolderRequest represents the request for deleting a folder. +type DeleteFolderRequest struct { + FolderID string +} + +// getAuthFromContext extracts Overleaf auth from context and validates it. +func getAuthFromContext(ctx context.Context) (*contextutil.OverleafAuth, error) { + auth, err := contextutil.GetOverleafAuth(ctx) + if err != nil { + return nil, fmt.Errorf("overleaf auth not found in context: %w", err) + } + if auth.Session == "" { + return nil, fmt.Errorf("overleaf session cookie is empty") + } + if auth.ProjectID == "" { + return nil, fmt.Errorf("overleaf project ID is empty") + } + if auth.CSRFToken == "" { + return nil, fmt.Errorf("overleaf csrf token is empty") + } + return auth, nil +} + +// buildCookieHeader builds the Cookie header value from auth. +func buildCookieHeader(auth *contextutil.OverleafAuth) string { + cookie := fmt.Sprintf("overleaf_session2=%s", auth.Session) + if auth.GCLB != "" { + cookie += fmt.Sprintf("; GCLB=%s", auth.GCLB) + } + return cookie +} + +// doRequest performs an HTTP request with common headers and error handling. +func (c *Client) doRequest(ctx context.Context, method, url string, body interface{}) ([]byte, error) { + auth, err := getAuthFromContext(ctx) + if err != nil { + return nil, err + } + + var reqBody io.Reader + if body != nil { + jsonBody, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("failed to marshal request body: %w", err) + } + reqBody = bytes.NewBuffer(jsonBody) + } + + httpReq, err := http.NewRequestWithContext(ctx, method, url, reqBody) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + httpReq.Header.Set("Accept", "application/json") + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Cookie", buildCookieHeader(auth)) + httpReq.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36") + + // Add CSRF token if available + if auth.CSRFToken != "" { + httpReq.Header.Set("X-Csrf-Token", auth.CSRFToken) + } else { + // warn if CSRF token is missing + fmt.Println("[WARN] CSRF Token is missing for overleaf client") + } + + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("overleaf API error: status=%d, body=%s", resp.StatusCode, string(respBody)) + } + + return respBody, nil +} + +// CreateDoc creates a new document in the Overleaf project. +// The project ID is taken from the context's OverleafAuth. +func (c *Client) CreateDoc(ctx context.Context, req *CreateDocRequest) (*CreateDocResponse, error) { + auth, err := getAuthFromContext(ctx) + if err != nil { + return nil, err + } + + url := fmt.Sprintf("%s/project/%s/doc", c.baseURL, auth.ProjectID) + respBody, err := c.doRequest(ctx, "POST", url, req) + if err != nil { + return nil, err + } + + var result CreateDocResponse + if err := json.Unmarshal(respBody, &result); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &result, nil +} + +// CreateFolder creates a new folder in the Overleaf project. +func (c *Client) CreateFolder(ctx context.Context, req *CreateFolderRequest) (*CreateFolderResponse, error) { + auth, err := getAuthFromContext(ctx) + if err != nil { + return nil, err + } + + url := fmt.Sprintf("%s/project/%s/folder", c.baseURL, auth.ProjectID) + respBody, err := c.doRequest(ctx, "POST", url, req) + if err != nil { + return nil, err + } + + var result CreateFolderResponse + if err := json.Unmarshal(respBody, &result); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &result, nil +} + +// DeleteDoc deletes a document from the Overleaf project. +func (c *Client) DeleteDoc(ctx context.Context, req *DeleteDocRequest) error { + auth, err := getAuthFromContext(ctx) + if err != nil { + return err + } + + url := fmt.Sprintf("%s/project/%s/doc/%s", c.baseURL, auth.ProjectID, req.DocID) + _, err = c.doRequest(ctx, "DELETE", url, nil) + return err +} + +// DeleteFolder deletes a folder from the Overleaf project. +func (c *Client) DeleteFolder(ctx context.Context, req *DeleteFolderRequest) error { + auth, err := getAuthFromContext(ctx) + if err != nil { + return err + } + + url := fmt.Sprintf("%s/project/%s/folder/%s", c.baseURL, auth.ProjectID, req.FolderID) + _, err = c.doRequest(ctx, "DELETE", url, nil) + return err +} diff --git a/internal/models/conversation.go b/internal/models/conversation.go index fdabf859..b060f43f 100644 --- a/internal/models/conversation.go +++ b/internal/models/conversation.go @@ -1,19 +1,39 @@ package models import ( + "fmt" + "strings" + "time" + + "github.com/google/uuid" "github.com/openai/openai-go/v2/responses" "github.com/openai/openai-go/v3" "go.mongodb.org/mongo-driver/v2/bson" ) +// Branch represents a single conversation branch created from message edits +type Branch struct { + ID string `bson:"id"` + CreatedAt bson.DateTime `bson:"created_at"` + UpdatedAt bson.DateTime `bson:"updated_at"` + InappChatHistory []bson.M `bson:"inapp_chat_history"` + OpenaiChatHistoryCompletion []openai.ChatCompletionMessageParamUnion `bson:"openai_chat_history_completion"` +} + type Conversation struct { - BaseModel `bson:",inline"` - UserID bson.ObjectID `bson:"user_id"` - ProjectID string `bson:"project_id"` - Title string `bson:"title"` - LanguageModel LanguageModel `bson:"language_model"` - ModelSlug string `bson:"model_slug"` - InappChatHistory []bson.M `bson:"inapp_chat_history"` // Store as raw BSON to avoid protobuf decoding issues + BaseModel `bson:",inline"` + UserID bson.ObjectID `bson:"user_id"` + ProjectID string `bson:"project_id"` + Title string `bson:"title"` + LanguageModel LanguageModel `bson:"language_model"` + ModelSlug string `bson:"model_slug"` + + // Multiple branches for edit history + Branches []Branch `bson:"branches"` + + // Below are legacy fields - kept for backward compatibility with old data + // When Branches is empty, use these as fallback (treated as default branch) + InappChatHistory []bson.M `bson:"inapp_chat_history"` OpenaiChatHistory responses.ResponseInputParam `bson:"openai_chat_history"` // The actual chat history sent to GPT OpenaiChatParams responses.ResponseNewParams `bson:"openai_chat_params"` // Conversation parameters, such as temperature, etc. @@ -24,3 +44,150 @@ type Conversation struct { func (c Conversation) CollectionName() string { return "conversations" } + +// GetActiveBranch returns the most recently updated branch. +// If no branches exist, returns nil (caller should use legacy fields). +func (c *Conversation) GetActiveBranch() *Branch { + if len(c.Branches) == 0 { + return nil + } + + var activeBranch *Branch + var latestUpdate bson.DateTime + for i := range c.Branches { + if c.Branches[i].UpdatedAt >= latestUpdate { + latestUpdate = c.Branches[i].UpdatedAt + activeBranch = &c.Branches[i] + } + } + return activeBranch +} + +// GetBranchByID returns the branch with the given ID, or nil if not found. +func (c *Conversation) GetBranchByID(branchID string) *Branch { + for i := range c.Branches { + if c.Branches[i].ID == branchID { + return &c.Branches[i] + } + } + return nil +} + +// GetBranchIndex returns the 1-indexed position of the branch with the given ID. +// Returns 0 if not found. Branches are sorted by CreatedAt. +func (c *Conversation) GetBranchIndex(branchID string) int { + for i := range c.Branches { + if c.Branches[i].ID == branchID { + return i + 1 + } + } + return 0 +} + +// CreateNewBranch creates a new branch based on an existing branch, +// truncated after the specified message ID. +// If baseBranchID is empty and there are no branches, uses legacy fields. +// Returns the new branch (already appended to c.Branches) and an error if +// truncateAfterMsgID is provided but the message is not found in the history. +func (c *Conversation) CreateNewBranch(baseBranchID string, truncateAfterMsgID string) (*Branch, error) { + now := bson.NewDateTimeFromTime(time.Now()) + newBranch := Branch{ + ID: uuid.New().String(), + CreatedAt: now, + UpdatedAt: now, + } + + // Get source history + var sourceInappHistory []bson.M + var sourceOpenaiHistory []openai.ChatCompletionMessageParamUnion + + if baseBranchID != "" { + baseBranch := c.GetBranchByID(baseBranchID) + if baseBranch != nil { + sourceInappHistory = baseBranch.InappChatHistory + sourceOpenaiHistory = baseBranch.OpenaiChatHistoryCompletion + } + } + + // Fallback to legacy fields if no base branch found + if sourceInappHistory == nil { + if len(c.Branches) > 0 { + // Use active branch + activeBranch := c.GetActiveBranch() + if activeBranch != nil { + sourceInappHistory = activeBranch.InappChatHistory + sourceOpenaiHistory = activeBranch.OpenaiChatHistoryCompletion + } + } else { + // Use legacy fields + sourceInappHistory = c.InappChatHistory + sourceOpenaiHistory = c.OpenaiChatHistoryCompletion + } + } + + // Handle truncation + if truncateAfterMsgID == "root" { + // Clear all history, keep only system message + newBranch.InappChatHistory = []bson.M{} + if len(sourceOpenaiHistory) > 0 { + newBranch.OpenaiChatHistoryCompletion = sourceOpenaiHistory[:1] + } else { + newBranch.OpenaiChatHistoryCompletion = []openai.ChatCompletionMessageParamUnion{} + } + } else if truncateAfterMsgID != "" { + // Find parent message and truncate after it + foundIndex := -1 + for i, msg := range sourceInappHistory { + if id, ok := msg["messageId"].(string); ok && strings.Contains(id, truncateAfterMsgID) { + foundIndex = i + break + } + } + + if foundIndex == -1 { + // Parent message not found - return error instead of silently copying entire history + return nil, fmt.Errorf("parent message with ID %q not found in conversation history", truncateAfterMsgID) + } + + // Copy up to and including the parent message + newBranch.InappChatHistory = make([]bson.M, foundIndex+1) + copy(newBranch.InappChatHistory, sourceInappHistory[:foundIndex+1]) + + // Map index: Inapp[i] -> Openai[i+1] (because Openai[0] is system) + if len(sourceOpenaiHistory) > foundIndex+1 { + newBranch.OpenaiChatHistoryCompletion = make([]openai.ChatCompletionMessageParamUnion, foundIndex+2) + copy(newBranch.OpenaiChatHistoryCompletion, sourceOpenaiHistory[:foundIndex+2]) + } else { + newBranch.OpenaiChatHistoryCompletion = make([]openai.ChatCompletionMessageParamUnion, len(sourceOpenaiHistory)) + copy(newBranch.OpenaiChatHistoryCompletion, sourceOpenaiHistory) + } + } else { + // No truncation, copy entire history + newBranch.InappChatHistory = make([]bson.M, len(sourceInappHistory)) + copy(newBranch.InappChatHistory, sourceInappHistory) + newBranch.OpenaiChatHistoryCompletion = make([]openai.ChatCompletionMessageParamUnion, len(sourceOpenaiHistory)) + copy(newBranch.OpenaiChatHistoryCompletion, sourceOpenaiHistory) + } + + c.Branches = append(c.Branches, newBranch) + return &c.Branches[len(c.Branches)-1], nil +} + +// EnsureBranches migrates legacy data to branch structure if needed. +// Call this when loading a conversation that might have old data format. +// Returns true if migration occurred (caller should persist the conversation). +func (c *Conversation) EnsureBranches() bool { + if len(c.Branches) == 0 && len(c.InappChatHistory) > 0 { + // Migrate legacy data to first branch + now := bson.NewDateTimeFromTime(time.Now()) + c.Branches = []Branch{{ + ID: uuid.New().String(), + CreatedAt: c.CreatedAt, + UpdatedAt: now, + InappChatHistory: c.InappChatHistory, + OpenaiChatHistoryCompletion: c.OpenaiChatHistoryCompletion, + }} + return true + } + return false +} diff --git a/internal/models/project.go b/internal/models/project.go index 0a28c064..54564d0f 100644 --- a/internal/models/project.go +++ b/internal/models/project.go @@ -56,3 +56,73 @@ func (u *Project) GetFullContent() (string, error) { func (u *Project) IsOutOfDate() bool { return u.UpdatedAt.Time().Before(time.Now().Add(-time.Minute * 30)) } + +// ProjectFolder represents a folder in the project hierarchy (v2) +type ProjectFolder struct { + ID string `bson:"id"` + Name string `bson:"name"` + Docs []ProjectDoc `bson:"docs"` + Folders []ProjectFolder `bson:"folders"` +} + +// ProjectV2 represents a project with hierarchical folder structure +type ProjectV2 struct { + BaseModel `bson:",inline"` + UserID bson.ObjectID `bson:"user_id"` + ProjectID string `bson:"project_id"` + Name string `bson:"name"` + RootDocID string `bson:"root_doc_id"` + RootFolder *ProjectFolder `bson:"root_folder"` + Instructions string `bson:"instructions"` + Category ClassifyPaperResponse `bson:"category,omitempty"` +} + +func (u ProjectV2) CollectionName() string { + return "projects" +} + +// collectDocs recursively collects all documents from the folder tree +func (u *ProjectV2) collectDocs(folder *ProjectFolder, docs map[string]string) { + if folder == nil { + return + } + for _, doc := range folder.Docs { + docs[doc.Filepath] = strings.Join(doc.Lines, "\n") + } + for i := range folder.Folders { + u.collectDocs(&folder.Folders[i], docs) + } +} + +// findRootDoc finds the root document in the folder tree +func (u *ProjectV2) findRootDoc(folder *ProjectFolder) *ProjectDoc { + if folder == nil { + return nil + } + for i := range folder.Docs { + if folder.Docs[i].ID == u.RootDocID { + return &folder.Docs[i] + } + } + for i := range folder.Folders { + if doc := u.findRootDoc(&folder.Folders[i]); doc != nil { + return doc + } + } + return nil +} + +func (u *ProjectV2) GetFullContent() (string, error) { + docs := make(map[string]string) + u.collectDocs(u.RootFolder, docs) + + rootDoc := u.findRootDoc(u.RootFolder) + if rootDoc == nil { + return "", shared.ErrInternal("root doc not found") + } + return tex.Latexpand(docs, rootDoc.Filepath) +} + +func (u *ProjectV2) IsOutOfDate() bool { + return u.UpdatedAt.Time().Before(time.Now().Add(-time.Minute * 30)) +} diff --git a/internal/services/project_v2.go b/internal/services/project_v2.go new file mode 100644 index 00000000..7c777cd5 --- /dev/null +++ b/internal/services/project_v2.go @@ -0,0 +1,117 @@ +package services + +import ( + "context" + "errors" + "time" + + "paperdebugger/internal/models" + + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" +) + +// GetProjectV2 retrieves a project with v2 structure +func (s *ProjectService) GetProjectV2(ctx context.Context, userID bson.ObjectID, projectID string) (*models.ProjectV2, error) { + result := s.projectCollection.FindOne(ctx, bson.M{"user_id": userID, "project_id": projectID}) + if result.Err() != nil { + return nil, result.Err() + } + + var project models.ProjectV2 + if err := result.Decode(&project); err != nil { + return nil, err + } + + return &project, nil +} + +// UpsertProjectV2 creates or updates a project with v2 structure +func (s *ProjectService) UpsertProjectV2(ctx context.Context, userID bson.ObjectID, projectID string, project *models.ProjectV2) (*models.ProjectV2, error) { + existingProject, err := s.GetProjectV2(ctx, userID, projectID) + if err != nil && err != mongo.ErrNoDocuments { + return nil, err + } + + if err == mongo.ErrNoDocuments { + // Create new project + project.ID = bson.NewObjectID() + project.CreatedAt = bson.NewDateTimeFromTime(time.Now()) + project.UpdatedAt = bson.NewDateTimeFromTime(time.Now()) + project.ProjectID = projectID + project.UserID = userID + _, err := s.projectCollection.InsertOne(ctx, project) + if err != nil { + return nil, err + } + return project, nil + } + + // Update existing project - check version conflicts + if err := s.checkVersionConflicts(project.RootFolder, existingProject.RootFolder); err != nil { + return nil, err + } + + // Preserve metadata + project.ID = existingProject.ID + project.CreatedAt = existingProject.CreatedAt + project.UpdatedAt = bson.NewDateTimeFromTime(time.Now()) + project.ProjectID = existingProject.ProjectID + project.UserID = existingProject.UserID + + // Full replacement update + _, err = s.projectCollection.UpdateOne(ctx, bson.M{"_id": existingProject.ID}, bson.M{"$set": project}) + if err != nil { + return nil, err + } + + return project, nil +} + +// checkVersionConflicts recursively checks for version conflicts in the folder tree +func (s *ProjectService) checkVersionConflicts(newFolder, existingFolder *models.ProjectFolder) error { + if newFolder == nil || existingFolder == nil { + return nil + } + + // Build a map of existing documents for quick lookup + existingDocs := s.flattenDocs(existingFolder) + + // Check all new documents against existing versions + newDocs := s.flattenDocs(newFolder) + for docID, newDoc := range newDocs { + if existingDoc, ok := existingDocs[docID]; ok { + if existingDoc.Version > newDoc.Version { + return errors.New("doc version is less than existing doc version") + } + } + } + + return nil +} + +// flattenDocs recursively flattens all documents in a folder tree into a map +func (s *ProjectService) flattenDocs(folder *models.ProjectFolder) map[string]models.ProjectDoc { + docs := make(map[string]models.ProjectDoc) + if folder == nil { + return docs + } + + s.collectDocsFromFolder(folder, docs) + return docs +} + +// collectDocsFromFolder is a helper that recursively collects documents +func (s *ProjectService) collectDocsFromFolder(folder *models.ProjectFolder, docs map[string]models.ProjectDoc) { + if folder == nil { + return + } + + for _, doc := range folder.Docs { + docs[doc.ID] = doc + } + + for i := range folder.Folders { + s.collectDocsFromFolder(&folder.Folders[i], docs) + } +} diff --git a/internal/services/toolkit/client/completion_v2.go b/internal/services/toolkit/client/completion_v2.go index e7e5b7b2..c0467097 100644 --- a/internal/services/toolkit/client/completion_v2.go +++ b/internal/services/toolkit/client/completion_v2.go @@ -6,6 +6,7 @@ import ( "paperdebugger/internal/models" "paperdebugger/internal/services/toolkit/handler" chatv2 "paperdebugger/pkg/gen/api/chat/v2" + "time" "github.com/openai/openai-go/v3" ) @@ -70,18 +71,25 @@ func (a *AIClientV2) ChatCompletionStreamV2(ctx context.Context, callbackStream for { params.Messages = openaiChatHistory // var openaiOutput OpenAIChatHistory + streamStartTime := time.Now() + _ = streamStartTime stream := oaiClient.Chat.Completions.NewStreaming(context.Background(), params) reasoning_content := "" answer_content := "" answer_content_id := "" - is_answering := false + has_sent_part_begin := false + firstChunkReceived := false tool_info := map[int]map[string]string{} toolCalls := []openai.FinishedChatCompletionToolCall{} for stream.Next() { // time.Sleep(5000 * time.Millisecond) // DEBUG POINT: change this to test in a slow mode chunk := stream.Current() + if !firstChunkReceived { + firstChunkReceived = true + } + if len(chunk.Choices) == 0 { // Handle usage information // fmt.Printf("Usage: %+v\n", chunk.Usage) @@ -90,21 +98,33 @@ func (a *AIClientV2) ChatCompletionStreamV2(ctx context.Context, callbackStream delta := chunk.Choices[0].Delta + // Send StreamPartBegin before any content (reasoning or answer) to ensure + // the frontend has created the assistant message part before receiving chunks. + // This is critical for models that send reasoning_content before regular content. + // We use HandleAssistantPartBegin instead of HandleAddedItem because the first + // chunk with reasoning content may not have delta.Role set to "assistant". + _, hasReasoningContent := delta.JSON.ExtraFields["reasoning_content"] + _, hasReasoning := delta.JSON.ExtraFields["reasoning"] + if !has_sent_part_begin && (delta.Role == "assistant" || delta.Content != "" || hasReasoningContent || hasReasoning) { + has_sent_part_begin = true + streamHandler.HandleAssistantPartBegin(chunk.ID) + } + if field, ok := delta.JSON.ExtraFields["reasoning_content"]; ok && field.Raw() != "null" { var s string err := json.Unmarshal([]byte(field.Raw()), &s) - if err != nil { - // fmt.Println(err) + if err == nil { + reasoning_content += s + streamHandler.HandleReasoningDelta(chunk.ID, s) } - reasoning_content += s - // fmt.Print(s) - } else { - if !is_answering { - is_answering = true - // fmt.Println("\n\n========== Response ==========") - streamHandler.HandleAddedItem(chunk) + } else if field, ok := delta.JSON.ExtraFields["reasoning"]; ok && field.Raw() != "null" { + var s string + err := json.Unmarshal([]byte(field.Raw()), &s) + if err == nil { + reasoning_content += s + streamHandler.HandleReasoningDelta(chunk.ID, s) } - + } else { if delta.Content != "" { answer_content += delta.Content answer_content_id = chunk.ID @@ -155,7 +175,7 @@ func (a *AIClientV2) ChatCompletionStreamV2(ctx context.Context, callbackStream // fmt.Printf("FinishReason: %s\n", chunk.Choices[0].FinishReason) // answer_content += chunk.Choices[0].Delta.Content // fmt.Printf("answer_content: %s\n", answer_content) - streamHandler.HandleTextDoneItem(chunk, answer_content) + streamHandler.HandleTextDoneItem(chunk, answer_content, reasoning_content) break } } diff --git a/internal/services/toolkit/client/utils_v2.go b/internal/services/toolkit/client/utils_v2.go index 65747ef8..5ebdac9d 100644 --- a/internal/services/toolkit/client/utils_v2.go +++ b/internal/services/toolkit/client/utils_v2.go @@ -12,6 +12,8 @@ import ( "paperdebugger/internal/libs/logger" "paperdebugger/internal/services" "paperdebugger/internal/services/toolkit/registry" + filetools "paperdebugger/internal/services/toolkit/tools/files" + latextools "paperdebugger/internal/services/toolkit/tools/latex" "paperdebugger/internal/services/toolkit/tools/xtramcp" chatv2 "paperdebugger/pkg/gen/api/chat/v2" "strings" @@ -109,40 +111,34 @@ func initializeToolkitV2( ) *registry.ToolRegistryV2 { toolRegistry := registry.NewToolRegistryV2() - // Register static file tools (create/delete don't need ProjectService - they're placeholder only) - // toolRegistry.Register("create_file", filetools.CreateFileToolDescriptionV2, filetools.CreateFileTool) - // toolRegistry.Register("delete_file", filetools.DeleteFileToolDescriptionV2, filetools.DeleteFileTool) - // toolRegistry.Register("create_folder", filetools.CreateFolderToolDescriptionV2, filetools.CreateFolderTool) - // toolRegistry.Register("delete_folder", filetools.DeleteFolderToolDescriptionV2, filetools.DeleteFolderTool) - - // Register file tools with ProjectService injection - // readFileTool := filetools.NewReadFileTool(projectService) - // toolRegistry.Register("read_file", filetools.ReadFileToolDescriptionV2, readFileTool.Call) - - // listFolderTool := filetools.NewListFolderTool(projectService) - // toolRegistry.Register("list_folder", filetools.ListFolderToolDescriptionV2, listFolderTool.Call) - - // searchStringTool := filetools.NewSearchStringTool(projectService) - // toolRegistry.Register("search_string", filetools.SearchStringToolDescriptionV2, searchStringTool.Call) - - // searchFileTool := filetools.NewSearchFileTool(projectService) - // toolRegistry.Register("search_file", filetools.SearchFileToolDescriptionV2, searchFileTool.Call) - - logger.Info("[AI Client V2] Registered static file tools", "count", 0) - - // Register LaTeX tools with ProjectService injection - // documentStructureTool := latextools.NewDocumentStructureTool(projectService) - // toolRegistry.Register("get_document_structure", latextools.GetDocumentStructureToolDescriptionV2, documentStructureTool.Call) - - // toolRegistry.Register("locate_section", latextools.LocateSectionToolDescriptionV2, latextools.LocateSectionTool) - - // readSectionSourceTool := latextools.NewReadSectionSourceTool(projectService) - // toolRegistry.Register("read_section_source", latextools.ReadSectionSourceToolDescriptionV2, readSectionSourceTool.Call) - - // readSourceLineRangeTool := latextools.NewReadSourceLineRangeTool(projectService) - // toolRegistry.Register("read_source_line_range", latextools.ReadSourceLineRangeToolDescriptionV2, readSourceLineRangeTool.Call) - - logger.Info("[AI Client V2] Registered static LaTeX tools", "count", 0) + // File tools with ProjectService dependency + createFileTool := filetools.NewCreateFileTool(projectService) + createFolderTool := filetools.NewCreateFolderTool(projectService) + readFileTool := filetools.NewReadFileTool(projectService) + listFolderTool := filetools.NewListFolderTool(projectService) + searchStringTool := filetools.NewSearchStringTool(projectService) + searchFileTool := filetools.NewSearchFileTool(projectService) + + // LaTeX tools with ProjectService dependency + documentStructureTool := latextools.NewDocumentStructureTool(projectService) + readSectionSourceTool := latextools.NewReadSectionSourceTool(projectService) + readSourceLineRangeTool := latextools.NewReadSourceLineRangeTool(projectService) + + // Register file tools + toolRegistry.Register("create_file", filetools.CreateFileToolDescriptionV2, createFileTool.Call) + toolRegistry.Register("delete_file", filetools.DeleteFileToolDescriptionV2, filetools.DeleteFileTool) + toolRegistry.Register("create_folder", filetools.CreateFolderToolDescriptionV2, createFolderTool.Call) + toolRegistry.Register("delete_folder", filetools.DeleteFolderToolDescriptionV2, filetools.DeleteFolderTool) + toolRegistry.Register("read_file", filetools.ReadFileToolDescriptionV2, readFileTool.Call) + toolRegistry.Register("list_folder", filetools.ListFolderToolDescriptionV2, listFolderTool.Call) + toolRegistry.Register("search_string", filetools.SearchStringToolDescriptionV2, searchStringTool.Call) + toolRegistry.Register("search_file", filetools.SearchFileToolDescriptionV2, searchFileTool.Call) + + // Register LaTeX tools + toolRegistry.Register("get_document_structure", latextools.GetDocumentStructureToolDescriptionV2, documentStructureTool.Call) + toolRegistry.Register("locate_section", latextools.LocateSectionToolDescriptionV2, latextools.LocateSectionTool) + toolRegistry.Register("read_section_source", latextools.ReadSectionSourceToolDescriptionV2, readSectionSourceTool.Call) + toolRegistry.Register("read_source_line_range", latextools.ReadSourceLineRangeToolDescriptionV2, readSourceLineRangeTool.Call) // Load tools dynamically from backend xtraMCPLoader := xtramcp.NewXtraMCPLoaderV2(db, projectService, cfg.XtraMCPURI) diff --git a/internal/services/toolkit/handler/stream_v2.go b/internal/services/toolkit/handler/stream_v2.go index f95ca82d..862a0a42 100644 --- a/internal/services/toolkit/handler/stream_v2.go +++ b/internal/services/toolkit/handler/stream_v2.go @@ -41,6 +41,27 @@ func (h *StreamHandlerV2) SendInitialization() { }) } +// HandleAssistantPartBegin sends a StreamPartBegin message for an assistant message. +// Unlike HandleAddedItem, this doesn't check the delta.Role field, which is important +// because reasoning models may send reasoning_content before the role field is set. +func (h *StreamHandlerV2) HandleAssistantPartBegin(messageId string) { + if h.callbackStream == nil { + return + } + h.callbackStream.Send(&chatv2.CreateConversationMessageStreamResponse{ + ResponsePayload: &chatv2.CreateConversationMessageStreamResponse_StreamPartBegin{ + StreamPartBegin: &chatv2.StreamPartBegin{ + MessageId: messageId, + Payload: &chatv2.MessagePayload{ + MessageType: &chatv2.MessagePayload_Assistant{ + Assistant: &chatv2.MessageTypeAssistant{}, + }, + }, + }, + }, + }) +} + func (h *StreamHandlerV2) HandleAddedItem(chunk openai.ChatCompletionChunk) { if h.callbackStream == nil { return @@ -98,21 +119,28 @@ func (h *StreamHandlerV2) HandleAddedItem(chunk openai.ChatCompletionChunk) { } } -func (h *StreamHandlerV2) HandleTextDoneItem(chunk openai.ChatCompletionChunk, content string) { +func (h *StreamHandlerV2) HandleTextDoneItem(chunk openai.ChatCompletionChunk, content string, reasoning string) { if h.callbackStream == nil { return } + assistant := &chatv2.MessageTypeAssistant{ + Content: content, + ModelSlug: h.modelSlug, + } + + // Only send Reasoning if it's not empty + if reasoning != "" { + assistant.Reasoning = &reasoning + } + h.callbackStream.Send(&chatv2.CreateConversationMessageStreamResponse{ ResponsePayload: &chatv2.CreateConversationMessageStreamResponse_StreamPartEnd{ StreamPartEnd: &chatv2.StreamPartEnd{ MessageId: chunk.ID, Payload: &chatv2.MessagePayload{ MessageType: &chatv2.MessagePayload_Assistant{ - Assistant: &chatv2.MessageTypeAssistant{ - Content: content, - ModelSlug: h.modelSlug, - }, + Assistant: assistant, }, }, }, @@ -155,6 +183,20 @@ func (h *StreamHandlerV2) HandleTextDelta(chunk openai.ChatCompletionChunk) { }) } +func (h *StreamHandlerV2) HandleReasoningDelta(messageId string, delta string) { + if h.callbackStream == nil { + return + } + h.callbackStream.Send(&chatv2.CreateConversationMessageStreamResponse{ + ResponsePayload: &chatv2.CreateConversationMessageStreamResponse_ReasoningChunk{ + ReasoningChunk: &chatv2.ReasoningChunk{ + MessageId: messageId, + Delta: delta, + }, + }, + }) +} + func (h *StreamHandlerV2) SendIncompleteIndicator(reason string, responseId string) { if h.callbackStream == nil { return diff --git a/internal/services/toolkit/tools/files/file_create.go b/internal/services/toolkit/tools/files/file_create.go index 54f91ecc..83f500f5 100644 --- a/internal/services/toolkit/tools/files/file_create.go +++ b/internal/services/toolkit/tools/files/file_create.go @@ -4,6 +4,14 @@ import ( "context" "encoding/json" "fmt" + "path/filepath" + "strings" + + "paperdebugger/internal/libs/contextutil" + "paperdebugger/internal/libs/overleaf" + "paperdebugger/internal/models" + "paperdebugger/internal/services" + "paperdebugger/internal/services/toolkit" "github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3/packages/param" @@ -13,37 +21,215 @@ var CreateFileToolDescriptionV2 = openai.ChatCompletionToolUnionParam{ OfFunction: &openai.ChatCompletionFunctionToolParam{ Function: openai.FunctionDefinitionParam{ Name: "create_file", - Description: param.NewOpt("Creates a new file at the specified path with the given content. Returns an error if the file already exists."), + Description: param.NewOpt("Creates a new file in the Overleaf project. The file will be created in the appropriate folder based on the path. Note: The file will be created empty initially; use edit tools to add content after creation."), Parameters: openai.FunctionParameters{ "type": "object", "properties": map[string]interface{}{ "path": map[string]any{ "type": "string", - "description": "The absolute or relative path where the file should be created.", - }, - "content": map[string]any{ - "type": "string", - "description": "The content to write to the file.", + "description": "The path where the file should be created (e.g., 'sections/introduction.tex', 'figures/diagram.tex'). Use forward slashes for directory separators. The parent folder must exist.", }, }, - "required": []string{"path", "content"}, + "required": []string{"path"}, }, }, }, } type CreateFileArgs struct { - Path string `json:"path"` - Content string `json:"content"` + Path string `json:"path"` +} + +type CreateFileTool struct { + projectService *services.ProjectService + overleafClient *overleaf.Client +} + +func NewCreateFileTool(projectService *services.ProjectService) *CreateFileTool { + return &CreateFileTool{ + projectService: projectService, + overleafClient: overleaf.NewClient(), + } } -func CreateFileTool(ctx context.Context, toolCallId string, args json.RawMessage) (string, string, error) { - var getArgs CreateFileArgs +func (t *CreateFileTool) Call(ctx context.Context, toolCallId string, args json.RawMessage) (string, string, error) { + var createArgs CreateFileArgs + if err := json.Unmarshal(args, &createArgs); err != nil { + return "", "", fmt.Errorf("invalid arguments: %w", err) + } + + // Validate path + if createArgs.Path == "" { + return "", "", fmt.Errorf("path is required") + } + + // Check Overleaf auth is available + auth, err := contextutil.GetOverleafAuth(ctx) + if err != nil { + return "", "", fmt.Errorf("overleaf authentication required for create_file: %w", err) + } + + // Get actor and project from context + actor, projectId, _ := toolkit.GetActorProjectConversationID(ctx) + if actor == nil || projectId == "" { + return "", "", fmt.Errorf("failed to get actor or project id from context") + } + + // Get project to find folder structure + projectV2, err := t.projectService.GetProjectV2(ctx, actor.ID, projectId) + if err != nil { + return "", "", fmt.Errorf("failed to get project: %w", err) + } + + // Check if file already exists + normalizedPath := normalizePath(createArgs.Path) + if t.fileExists(projectV2.RootFolder, normalizedPath) { + return fmt.Sprintf("File already exists: %s", createArgs.Path), "", nil + } - if err := json.Unmarshal(args, &getArgs); err != nil { + // Resolve parent folder ID from path + parentFolderID, err := t.resolveParentFolderID(projectV2, normalizedPath) + if err != nil { return "", "", err } - // TODO: Implement actual file creation logic - return fmt.Sprintf("[DUMMY] File created at: %s (content length: %d bytes)", getArgs.Path, len(getArgs.Content)), "", nil + fileName := filepath.Base(normalizedPath) + + // Call Overleaf API to create the file + resp, err := t.overleafClient.CreateDoc(ctx, &overleaf.CreateDocRequest{ + ParentFolderID: parentFolderID, + Name: fileName, + }) + if err != nil { + // Check for specific errors + if strings.Contains(err.Error(), "401") || strings.Contains(err.Error(), "403") { + return "", "", fmt.Errorf("overleaf authentication failed - session may have expired: %w", err) + } + return "", "", fmt.Errorf("failed to create file in Overleaf: %w", err) + } + + result := fmt.Sprintf("Successfully created file '%s' (id: %s) at path '%s'.\n\nNote: The file is currently empty. To add content, use the appropriate edit tool after the document list is refreshed.", + fileName, resp.ID, createArgs.Path) + + // Instruction for the LLM about next steps + instruction := "The file has been created in Overleaf. The user's document list will be refreshed automatically. You can proceed with other tasks or wait for confirmation before editing the file content." + + _ = auth // Used for logging context, suppress unused warning + + return result, instruction, nil +} + +// fileExists checks if a file already exists at the given path in the project +func (t *CreateFileTool) fileExists(folder *models.ProjectFolder, targetPath string) bool { + if folder == nil { + return false + } + return t.findDocInFolder(folder, targetPath, "") != nil +} + +// findDocInFolder recursively searches for a document by path +func (t *CreateFileTool) findDocInFolder(folder *models.ProjectFolder, targetPath, currentPath string) *models.ProjectDoc { + if folder == nil { + return nil + } + + // Build current path + folderPath := currentPath + if folder.Name != "" && folder.Name != "rootFolder" { + if folderPath == "" { + folderPath = folder.Name + } else { + folderPath = folderPath + "/" + folder.Name + } + } + + // Check docs in this folder + for i := range folder.Docs { + docPath := folderPath + if docPath == "" { + docPath = folder.Docs[i].Filepath + } else { + docPath = folderPath + "/" + filepath.Base(folder.Docs[i].Filepath) + } + docPath = normalizePath(docPath) + + if docPath == targetPath || normalizePath(folder.Docs[i].Filepath) == targetPath { + return &folder.Docs[i] + } + } + + // Recurse into subfolders + for i := range folder.Folders { + if doc := t.findDocInFolder(&folder.Folders[i], targetPath, folderPath); doc != nil { + return doc + } + } + + return nil +} + +// resolveParentFolderID finds the folder ID for the parent directory of the given path +func (t *CreateFileTool) resolveParentFolderID(project *models.ProjectV2, targetPath string) (string, error) { + if project.RootFolder == nil { + return "", fmt.Errorf("project has no folder structure") + } + + // Get directory part of the path + dir := filepath.Dir(targetPath) + + // If creating in root, return root folder ID + if dir == "." || dir == "/" || dir == "" { + return project.RootFolder.ID, nil + } + + // Clean and normalize the directory path + dir = normalizePath(dir) + + // Find the folder by path + folder := t.findFolderByPath(project.RootFolder, dir, "") + if folder == nil { + return "", fmt.Errorf("parent folder '%s' not found. Please create the folder first using create_folder tool, or create the file in the root directory", dir) + } + + return folder.ID, nil +} + +// findFolderByPath recursively finds a folder by its path +func (t *CreateFileTool) findFolderByPath(folder *models.ProjectFolder, targetPath, currentPath string) *models.ProjectFolder { + if folder == nil { + return nil + } + + // Build current folder path + folderPath := currentPath + if folder.Name != "" && folder.Name != "rootFolder" { + if folderPath == "" { + folderPath = folder.Name + } else { + folderPath = folderPath + "/" + folder.Name + } + } + + // Check if this is the target folder + if folderPath == targetPath { + return folder + } + + // Recurse into subfolders + for i := range folder.Folders { + if found := t.findFolderByPath(&folder.Folders[i], targetPath, folderPath); found != nil { + return found + } + } + + return nil +} + +// Legacy function for backward compatibility +func CreateFileTool_Legacy(ctx context.Context, toolCallId string, args json.RawMessage) (string, string, error) { + var createArgs CreateFileArgs + if err := json.Unmarshal(args, &createArgs); err != nil { + return "", "", err + } + return "", "", fmt.Errorf("create_file tool requires initialization with ProjectService") } diff --git a/internal/services/toolkit/tools/files/file_delete.go b/internal/services/toolkit/tools/files/file_delete.go index 18f78ddd..448142db 100644 --- a/internal/services/toolkit/tools/files/file_delete.go +++ b/internal/services/toolkit/tools/files/file_delete.go @@ -40,5 +40,5 @@ func DeleteFileTool(ctx context.Context, toolCallId string, args json.RawMessage } // TODO: Implement actual file deletion logic - return fmt.Sprintf("[DUMMY] File deleted: %s", getArgs.Path), "", nil + return "", "", fmt.Errorf("delete_file tool is not yet implemented: cannot delete file at %s", getArgs.Path) } diff --git a/internal/services/toolkit/tools/files/file_search.go b/internal/services/toolkit/tools/files/file_search.go index 790d05f8..81c3b8f2 100644 --- a/internal/services/toolkit/tools/files/file_search.go +++ b/internal/services/toolkit/tools/files/file_search.go @@ -104,7 +104,7 @@ func (t *SearchFileTool) Call(ctx context.Context, toolCallId string, args json. if !recursive && dir != searchPath { continue } - if recursive && !strings.HasPrefix(docPath, searchPath) { + if recursive && !strings.HasPrefix(docPath, searchPath+"/") { continue } } diff --git a/internal/services/toolkit/tools/files/folder_create.go b/internal/services/toolkit/tools/files/folder_create.go index 4c5a1c4e..e8b6b1ed 100644 --- a/internal/services/toolkit/tools/files/folder_create.go +++ b/internal/services/toolkit/tools/files/folder_create.go @@ -4,6 +4,14 @@ import ( "context" "encoding/json" "fmt" + "path/filepath" + "strings" + + "paperdebugger/internal/libs/contextutil" + "paperdebugger/internal/libs/overleaf" + "paperdebugger/internal/models" + "paperdebugger/internal/services" + "paperdebugger/internal/services/toolkit" "github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3/packages/param" @@ -13,13 +21,13 @@ var CreateFolderToolDescriptionV2 = openai.ChatCompletionToolUnionParam{ OfFunction: &openai.ChatCompletionFunctionToolParam{ Function: openai.FunctionDefinitionParam{ Name: "create_folder", - Description: param.NewOpt("Creates a new folder (directory) at the specified path. Creates parent directories if they don't exist (like mkdir -p)."), + Description: param.NewOpt("Creates a new folder in the Overleaf project. The parent folder must exist. Use forward slashes for directory separators (e.g., 'sections/subsections')."), Parameters: openai.FunctionParameters{ "type": "object", "properties": map[string]interface{}{ "path": map[string]any{ "type": "string", - "description": "The absolute or relative path where the folder should be created.", + "description": "The path where the folder should be created (e.g., 'sections', 'figures/diagrams'). Use forward slashes for directory separators. The parent folder must exist.", }, }, "required": []string{"path"}, @@ -32,13 +40,156 @@ type CreateFolderArgs struct { Path string `json:"path"` } -func CreateFolderTool(ctx context.Context, toolCallId string, args json.RawMessage) (string, string, error) { - var getArgs CreateFolderArgs +type CreateFolderTool struct { + projectService *services.ProjectService + overleafClient *overleaf.Client +} + +func NewCreateFolderTool(projectService *services.ProjectService) *CreateFolderTool { + return &CreateFolderTool{ + projectService: projectService, + overleafClient: overleaf.NewClient(), + } +} + +func (t *CreateFolderTool) Call(ctx context.Context, toolCallId string, args json.RawMessage) (string, string, error) { + var createArgs CreateFolderArgs + if err := json.Unmarshal(args, &createArgs); err != nil { + return "", "", fmt.Errorf("invalid arguments: %w", err) + } + + // Validate path + if createArgs.Path == "" { + return "", "", fmt.Errorf("path is required") + } + + // Check Overleaf auth is available + auth, err := contextutil.GetOverleafAuth(ctx) + if err != nil { + return "", "", fmt.Errorf("overleaf authentication required for create_folder: %w", err) + } + + // Get actor and project from context + actor, projectId, _ := toolkit.GetActorProjectConversationID(ctx) + if actor == nil || projectId == "" { + return "", "", fmt.Errorf("failed to get actor or project id from context") + } + + // Get project to find folder structure + projectV2, err := t.projectService.GetProjectV2(ctx, actor.ID, projectId) + if err != nil { + return "", "", fmt.Errorf("failed to get project: %w", err) + } + + // Normalize the path + normalizedPath := normalizePath(createArgs.Path) - if err := json.Unmarshal(args, &getArgs); err != nil { + // Check if folder already exists + if t.folderExists(projectV2.RootFolder, normalizedPath) { + return fmt.Sprintf("Folder already exists: %s", createArgs.Path), "", nil + } + + // Resolve parent folder ID from path + parentFolderID, err := t.resolveParentFolderID(projectV2, normalizedPath) + if err != nil { return "", "", err } - // TODO: Implement actual folder creation logic - return fmt.Sprintf("[DUMMY] Folder created at: %s", getArgs.Path), "", nil + folderName := filepath.Base(normalizedPath) + + // Call Overleaf API to create the folder + resp, err := t.overleafClient.CreateFolder(ctx, &overleaf.CreateFolderRequest{ + ParentFolderID: parentFolderID, + Name: folderName, + }) + if err != nil { + // Check for specific errors + if strings.Contains(err.Error(), "401") || strings.Contains(err.Error(), "403") { + return "", "", fmt.Errorf("overleaf authentication failed - session may have expired: %w", err) + } + return "", "", fmt.Errorf("failed to create folder in Overleaf: %w", err) + } + + result := fmt.Sprintf("Successfully created folder '%s' (id: %s) at path '%s'.", + folderName, resp.ID, createArgs.Path) + + instruction := "The folder has been created in Overleaf. The user's document list will be refreshed automatically. You can now create files inside this folder." + + _ = auth // Used for logging context, suppress unused warning + + return result, instruction, nil +} + +// folderExists checks if a folder already exists at the given path in the project +func (t *CreateFolderTool) folderExists(folder *models.ProjectFolder, targetPath string) bool { + if folder == nil { + return false + } + return t.findFolderByPath(folder, targetPath, "") != nil +} + +// findFolderByPath recursively finds a folder by its path +func (t *CreateFolderTool) findFolderByPath(folder *models.ProjectFolder, targetPath, currentPath string) *models.ProjectFolder { + if folder == nil { + return nil + } + + // Build current folder path + folderPath := currentPath + if folder.Name != "" && folder.Name != "rootFolder" { + if folderPath == "" { + folderPath = folder.Name + } else { + folderPath = folderPath + "/" + folder.Name + } + } + + // Check if this is the target folder + if folderPath == targetPath { + return folder + } + + // Recurse into subfolders + for i := range folder.Folders { + if found := t.findFolderByPath(&folder.Folders[i], targetPath, folderPath); found != nil { + return found + } + } + + return nil +} + +// resolveParentFolderID finds the folder ID for the parent directory of the given path +func (t *CreateFolderTool) resolveParentFolderID(project *models.ProjectV2, targetPath string) (string, error) { + if project.RootFolder == nil { + return "", fmt.Errorf("project has no folder structure") + } + + // Get directory part of the path + dir := filepath.Dir(targetPath) + + // If creating in root, return root folder ID + if dir == "." || dir == "/" || dir == "" { + return project.RootFolder.ID, nil + } + + // Clean and normalize the directory path + dir = normalizePath(dir) + + // Find the folder by path + folder := t.findFolderByPath(project.RootFolder, dir, "") + if folder == nil { + return "", fmt.Errorf("parent folder '%s' not found. Please create the parent folder first, or create the folder in the root directory", dir) + } + + return folder.ID, nil +} + +// Legacy function for backward compatibility +func CreateFolderTool_Legacy(ctx context.Context, toolCallId string, args json.RawMessage) (string, string, error) { + var createArgs CreateFolderArgs + if err := json.Unmarshal(args, &createArgs); err != nil { + return "", "", err + } + return "", "", fmt.Errorf("create_folder tool requires initialization with ProjectService") } diff --git a/internal/services/toolkit/tools/files/folder_delete.go b/internal/services/toolkit/tools/files/folder_delete.go index d18f979e..9a76685c 100644 --- a/internal/services/toolkit/tools/files/folder_delete.go +++ b/internal/services/toolkit/tools/files/folder_delete.go @@ -44,11 +44,6 @@ func DeleteFolderTool(ctx context.Context, toolCallId string, args json.RawMessa return "", "", err } - recursive := false - if getArgs.Recursive != nil { - recursive = *getArgs.Recursive - } - // TODO: Implement actual folder deletion logic - return fmt.Sprintf("[DUMMY] Folder deleted: %s (recursive: %v)", getArgs.Path, recursive), "", nil + return "", "", fmt.Errorf("delete_folder tool is not yet implemented: cannot delete folder at %s", getArgs.Path) } diff --git a/internal/services/toolkit/tools/latex/locate_section.go b/internal/services/toolkit/tools/latex/locate_section.go index d0b26830..df7d7d65 100644 --- a/internal/services/toolkit/tools/latex/locate_section.go +++ b/internal/services/toolkit/tools/latex/locate_section.go @@ -40,8 +40,5 @@ func LocateSectionTool(ctx context.Context, toolCallId string, args json.RawMess } // TODO: Implement actual section location logic - return fmt.Sprintf(`[DUMMY] Located section '%s': -File: main.tex -Start Line: 42 -End Line: 87`, getArgs.Title), "", nil + return "", "", fmt.Errorf("locate_section tool is not yet implemented: cannot locate section '%s'", getArgs.Title) } diff --git a/internal/wire.go b/internal/wire.go index f823bc2e..ab56ee47 100644 --- a/internal/wire.go +++ b/internal/wire.go @@ -9,6 +9,7 @@ import ( "paperdebugger/internal/api/chat" "paperdebugger/internal/api/comment" "paperdebugger/internal/api/project" + projectv2 "paperdebugger/internal/api/project/v2" "paperdebugger/internal/api/user" "paperdebugger/internal/libs/cfg" "paperdebugger/internal/libs/db" @@ -31,6 +32,7 @@ var Set = wire.NewSet( chat.NewChatServerV2, user.NewUserServer, project.NewProjectServer, + projectv2.NewProjectServerV2, comment.NewCommentServer, aiclient.NewAIClient, diff --git a/internal/wire_gen.go b/internal/wire_gen.go index 75c4e91a..0842eab0 100644 --- a/internal/wire_gen.go +++ b/internal/wire_gen.go @@ -13,6 +13,7 @@ import ( "paperdebugger/internal/api/chat" "paperdebugger/internal/api/comment" "paperdebugger/internal/api/project" + "paperdebugger/internal/api/project/v2" "paperdebugger/internal/api/user" "paperdebugger/internal/libs/cfg" "paperdebugger/internal/libs/db" @@ -44,8 +45,9 @@ func InitializeApp() (*api.Server, error) { promptService := services.NewPromptService(dbDB, cfgCfg, loggerLogger) userServiceServer := user.NewUserServer(userService, promptService, cfgCfg, loggerLogger) projectServiceServer := project.NewProjectServer(projectService, loggerLogger, cfgCfg) + projectv2ProjectServiceServer := v2.NewProjectServerV2(projectService, loggerLogger, cfgCfg) commentServiceServer := comment.NewCommentServer(projectService, chatService, reverseCommentService, loggerLogger, cfgCfg) - grpcServer := api.NewGrpcServer(userService, cfgCfg, authServiceServer, chatServiceServer, chatv2ChatServiceServer, userServiceServer, projectServiceServer, commentServiceServer) + grpcServer := api.NewGrpcServer(userService, cfgCfg, authServiceServer, chatServiceServer, chatv2ChatServiceServer, userServiceServer, projectServiceServer, projectv2ProjectServiceServer, commentServiceServer) oAuthService := services.NewOAuthService(dbDB, cfgCfg, loggerLogger) oAuthHandler := auth.NewOAuthHandler(oAuthService) ginServer := api.NewGinServer(cfgCfg, oAuthHandler) @@ -55,4 +57,4 @@ func InitializeApp() (*api.Server, error) { // wire.go: -var Set = wire.NewSet(api.NewServer, api.NewGrpcServer, api.NewGinServer, auth.NewOAuthHandler, auth.NewAuthServer, chat.NewChatServer, chat.NewChatServerV2, user.NewUserServer, project.NewProjectServer, comment.NewCommentServer, client.NewAIClient, client.NewAIClientV2, services.NewReverseCommentService, services.NewChatService, services.NewChatServiceV2, services.NewTokenService, services.NewUserService, services.NewProjectService, services.NewPromptService, services.NewOAuthService, cfg.GetCfg, logger.GetLogger, db.NewDB) +var Set = wire.NewSet(api.NewServer, api.NewGrpcServer, api.NewGinServer, auth.NewOAuthHandler, auth.NewAuthServer, chat.NewChatServer, chat.NewChatServerV2, user.NewUserServer, project.NewProjectServer, v2.NewProjectServerV2, comment.NewCommentServer, client.NewAIClient, client.NewAIClientV2, services.NewReverseCommentService, services.NewChatService, services.NewChatServiceV2, services.NewTokenService, services.NewUserService, services.NewProjectService, services.NewPromptService, services.NewOAuthService, cfg.GetCfg, logger.GetLogger, db.NewDB) diff --git a/pkg/gen/api/chat/v2/chat.pb.go b/pkg/gen/api/chat/v2/chat.pb.go index 0db41ebd..fc599e3b 100644 --- a/pkg/gen/api/chat/v2/chat.pb.go +++ b/pkg/gen/api/chat/v2/chat.pb.go @@ -10,6 +10,7 @@ import ( _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + v1 "paperdebugger/pkg/gen/api/shared/v1" reflect "reflect" sync "sync" unsafe "unsafe" @@ -236,6 +237,7 @@ type MessageTypeAssistant struct { state protoimpl.MessageState `protogen:"open.v1"` Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` ModelSlug string `protobuf:"bytes,2,opt,name=model_slug,json=modelSlug,proto3" json:"model_slug,omitempty"` + Reasoning *string `protobuf:"bytes,3,opt,name=reasoning,proto3,oneof" json:"reasoning,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -284,6 +286,13 @@ func (x *MessageTypeAssistant) GetModelSlug() string { return "" } +func (x *MessageTypeAssistant) GetReasoning() string { + if x != nil && x.Reasoning != nil { + return *x.Reasoning + } + return "" +} + type MessageTypeUser struct { state protoimpl.MessageState `protogen:"open.v1"` Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` @@ -594,20 +603,85 @@ func (x *Message) GetTimestamp() int64 { return 0 } +type BranchInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + CreatedAt int64 `protobuf:"varint,2,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt int64 `protobuf:"varint,3,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BranchInfo) Reset() { + *x = BranchInfo{} + mi := &file_chat_v2_chat_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BranchInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BranchInfo) ProtoMessage() {} + +func (x *BranchInfo) ProtoReflect() protoreflect.Message { + mi := &file_chat_v2_chat_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BranchInfo.ProtoReflect.Descriptor instead. +func (*BranchInfo) Descriptor() ([]byte, []int) { + return file_chat_v2_chat_proto_rawDescGZIP(), []int{8} +} + +func (x *BranchInfo) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *BranchInfo) GetCreatedAt() int64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +func (x *BranchInfo) GetUpdatedAt() int64 { + if x != nil { + return x.UpdatedAt + } + return 0 +} + type Conversation struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` ModelSlug string `protobuf:"bytes,3,opt,name=model_slug,json=modelSlug,proto3" json:"model_slug,omitempty"` // If list conversations, then messages length is 0. - Messages []*Message `protobuf:"bytes,4,rep,name=messages,proto3" json:"messages,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Messages []*Message `protobuf:"bytes,4,rep,name=messages,proto3" json:"messages,omitempty"` + // Branch information + CurrentBranchId string `protobuf:"bytes,5,opt,name=current_branch_id,json=currentBranchId,proto3" json:"current_branch_id,omitempty"` // ID of the branch being viewed + Branches []*BranchInfo `protobuf:"bytes,6,rep,name=branches,proto3" json:"branches,omitempty"` // All available branches + CurrentBranchIndex int32 `protobuf:"varint,7,opt,name=current_branch_index,json=currentBranchIndex,proto3" json:"current_branch_index,omitempty"` // 1-indexed position for UI + TotalBranches int32 `protobuf:"varint,8,opt,name=total_branches,json=totalBranches,proto3" json:"total_branches,omitempty"` // Total number of branches + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Conversation) Reset() { *x = Conversation{} - mi := &file_chat_v2_chat_proto_msgTypes[8] + mi := &file_chat_v2_chat_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -619,7 +693,7 @@ func (x *Conversation) String() string { func (*Conversation) ProtoMessage() {} func (x *Conversation) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[8] + mi := &file_chat_v2_chat_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -632,7 +706,7 @@ func (x *Conversation) ProtoReflect() protoreflect.Message { // Deprecated: Use Conversation.ProtoReflect.Descriptor instead. func (*Conversation) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{8} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{9} } func (x *Conversation) GetId() string { @@ -663,6 +737,34 @@ func (x *Conversation) GetMessages() []*Message { return nil } +func (x *Conversation) GetCurrentBranchId() string { + if x != nil { + return x.CurrentBranchId + } + return "" +} + +func (x *Conversation) GetBranches() []*BranchInfo { + if x != nil { + return x.Branches + } + return nil +} + +func (x *Conversation) GetCurrentBranchIndex() int32 { + if x != nil { + return x.CurrentBranchIndex + } + return 0 +} + +func (x *Conversation) GetTotalBranches() int32 { + if x != nil { + return x.TotalBranches + } + return 0 +} + type ListConversationsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` ProjectId *string `protobuf:"bytes,1,opt,name=project_id,json=projectId,proto3,oneof" json:"project_id,omitempty"` @@ -672,7 +774,7 @@ type ListConversationsRequest struct { func (x *ListConversationsRequest) Reset() { *x = ListConversationsRequest{} - mi := &file_chat_v2_chat_proto_msgTypes[9] + mi := &file_chat_v2_chat_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -684,7 +786,7 @@ func (x *ListConversationsRequest) String() string { func (*ListConversationsRequest) ProtoMessage() {} func (x *ListConversationsRequest) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[9] + mi := &file_chat_v2_chat_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -697,7 +799,7 @@ func (x *ListConversationsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListConversationsRequest.ProtoReflect.Descriptor instead. func (*ListConversationsRequest) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{9} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{10} } func (x *ListConversationsRequest) GetProjectId() string { @@ -717,7 +819,7 @@ type ListConversationsResponse struct { func (x *ListConversationsResponse) Reset() { *x = ListConversationsResponse{} - mi := &file_chat_v2_chat_proto_msgTypes[10] + mi := &file_chat_v2_chat_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -729,7 +831,7 @@ func (x *ListConversationsResponse) String() string { func (*ListConversationsResponse) ProtoMessage() {} func (x *ListConversationsResponse) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[10] + mi := &file_chat_v2_chat_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -742,7 +844,7 @@ func (x *ListConversationsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListConversationsResponse.ProtoReflect.Descriptor instead. func (*ListConversationsResponse) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{10} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{11} } func (x *ListConversationsResponse) GetConversations() []*Conversation { @@ -755,13 +857,14 @@ func (x *ListConversationsResponse) GetConversations() []*Conversation { type GetConversationRequest struct { state protoimpl.MessageState `protogen:"open.v1"` ConversationId string `protobuf:"bytes,1,opt,name=conversation_id,json=conversationId,proto3" json:"conversation_id,omitempty"` + BranchId *string `protobuf:"bytes,2,opt,name=branch_id,json=branchId,proto3,oneof" json:"branch_id,omitempty"` // Optional: specific branch to view unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *GetConversationRequest) Reset() { *x = GetConversationRequest{} - mi := &file_chat_v2_chat_proto_msgTypes[11] + mi := &file_chat_v2_chat_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -773,7 +876,7 @@ func (x *GetConversationRequest) String() string { func (*GetConversationRequest) ProtoMessage() {} func (x *GetConversationRequest) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[11] + mi := &file_chat_v2_chat_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -786,7 +889,7 @@ func (x *GetConversationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetConversationRequest.ProtoReflect.Descriptor instead. func (*GetConversationRequest) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{11} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{12} } func (x *GetConversationRequest) GetConversationId() string { @@ -796,6 +899,13 @@ func (x *GetConversationRequest) GetConversationId() string { return "" } +func (x *GetConversationRequest) GetBranchId() string { + if x != nil && x.BranchId != nil { + return *x.BranchId + } + return "" +} + type GetConversationResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Conversation *Conversation `protobuf:"bytes,1,opt,name=conversation,proto3" json:"conversation,omitempty"` @@ -805,7 +915,7 @@ type GetConversationResponse struct { func (x *GetConversationResponse) Reset() { *x = GetConversationResponse{} - mi := &file_chat_v2_chat_proto_msgTypes[12] + mi := &file_chat_v2_chat_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -817,7 +927,7 @@ func (x *GetConversationResponse) String() string { func (*GetConversationResponse) ProtoMessage() {} func (x *GetConversationResponse) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[12] + mi := &file_chat_v2_chat_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -830,7 +940,7 @@ func (x *GetConversationResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetConversationResponse.ProtoReflect.Descriptor instead. func (*GetConversationResponse) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{12} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{13} } func (x *GetConversationResponse) GetConversation() *Conversation { @@ -850,7 +960,7 @@ type UpdateConversationRequest struct { func (x *UpdateConversationRequest) Reset() { *x = UpdateConversationRequest{} - mi := &file_chat_v2_chat_proto_msgTypes[13] + mi := &file_chat_v2_chat_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -862,7 +972,7 @@ func (x *UpdateConversationRequest) String() string { func (*UpdateConversationRequest) ProtoMessage() {} func (x *UpdateConversationRequest) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[13] + mi := &file_chat_v2_chat_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -875,7 +985,7 @@ func (x *UpdateConversationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateConversationRequest.ProtoReflect.Descriptor instead. func (*UpdateConversationRequest) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{13} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{14} } func (x *UpdateConversationRequest) GetConversationId() string { @@ -901,7 +1011,7 @@ type UpdateConversationResponse struct { func (x *UpdateConversationResponse) Reset() { *x = UpdateConversationResponse{} - mi := &file_chat_v2_chat_proto_msgTypes[14] + mi := &file_chat_v2_chat_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -913,7 +1023,7 @@ func (x *UpdateConversationResponse) String() string { func (*UpdateConversationResponse) ProtoMessage() {} func (x *UpdateConversationResponse) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[14] + mi := &file_chat_v2_chat_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -926,7 +1036,7 @@ func (x *UpdateConversationResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateConversationResponse.ProtoReflect.Descriptor instead. func (*UpdateConversationResponse) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{14} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{15} } func (x *UpdateConversationResponse) GetConversation() *Conversation { @@ -945,7 +1055,7 @@ type DeleteConversationRequest struct { func (x *DeleteConversationRequest) Reset() { *x = DeleteConversationRequest{} - mi := &file_chat_v2_chat_proto_msgTypes[15] + mi := &file_chat_v2_chat_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -957,7 +1067,7 @@ func (x *DeleteConversationRequest) String() string { func (*DeleteConversationRequest) ProtoMessage() {} func (x *DeleteConversationRequest) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[15] + mi := &file_chat_v2_chat_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -970,7 +1080,7 @@ func (x *DeleteConversationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteConversationRequest.ProtoReflect.Descriptor instead. func (*DeleteConversationRequest) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{15} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{16} } func (x *DeleteConversationRequest) GetConversationId() string { @@ -988,7 +1098,7 @@ type DeleteConversationResponse struct { func (x *DeleteConversationResponse) Reset() { *x = DeleteConversationResponse{} - mi := &file_chat_v2_chat_proto_msgTypes[16] + mi := &file_chat_v2_chat_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1000,7 +1110,7 @@ func (x *DeleteConversationResponse) String() string { func (*DeleteConversationResponse) ProtoMessage() {} func (x *DeleteConversationResponse) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[16] + mi := &file_chat_v2_chat_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1013,24 +1123,26 @@ func (x *DeleteConversationResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteConversationResponse.ProtoReflect.Descriptor instead. func (*DeleteConversationResponse) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{16} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{17} } type SupportedModel struct { - state protoimpl.MessageState `protogen:"open.v1"` - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Slug string `protobuf:"bytes,2,opt,name=slug,proto3" json:"slug,omitempty"` - TotalContext int64 `protobuf:"varint,3,opt,name=total_context,json=totalContext,proto3" json:"total_context,omitempty"` - MaxOutput int64 `protobuf:"varint,4,opt,name=max_output,json=maxOutput,proto3" json:"max_output,omitempty"` - InputPrice int64 `protobuf:"varint,5,opt,name=input_price,json=inputPrice,proto3" json:"input_price,omitempty"` // in cents per 1M tokens - OutputPrice int64 `protobuf:"varint,6,opt,name=output_price,json=outputPrice,proto3" json:"output_price,omitempty"` // in cents per 1M tokens - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Slug string `protobuf:"bytes,2,opt,name=slug,proto3" json:"slug,omitempty"` + TotalContext int64 `protobuf:"varint,3,opt,name=total_context,json=totalContext,proto3" json:"total_context,omitempty"` + MaxOutput int64 `protobuf:"varint,4,opt,name=max_output,json=maxOutput,proto3" json:"max_output,omitempty"` + InputPrice int64 `protobuf:"varint,5,opt,name=input_price,json=inputPrice,proto3" json:"input_price,omitempty"` // in cents per 1M tokens + OutputPrice int64 `protobuf:"varint,6,opt,name=output_price,json=outputPrice,proto3" json:"output_price,omitempty"` // in cents per 1M tokens + Disabled bool `protobuf:"varint,7,opt,name=disabled,proto3" json:"disabled,omitempty"` // Whether the model is disabled + DisabledReason *string `protobuf:"bytes,8,opt,name=disabled_reason,json=disabledReason,proto3,oneof" json:"disabled_reason,omitempty"` // Reason why the model is disabled + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *SupportedModel) Reset() { *x = SupportedModel{} - mi := &file_chat_v2_chat_proto_msgTypes[17] + mi := &file_chat_v2_chat_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1042,7 +1154,7 @@ func (x *SupportedModel) String() string { func (*SupportedModel) ProtoMessage() {} func (x *SupportedModel) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[17] + mi := &file_chat_v2_chat_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1055,7 +1167,7 @@ func (x *SupportedModel) ProtoReflect() protoreflect.Message { // Deprecated: Use SupportedModel.ProtoReflect.Descriptor instead. func (*SupportedModel) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{17} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{18} } func (x *SupportedModel) GetName() string { @@ -1100,6 +1212,20 @@ func (x *SupportedModel) GetOutputPrice() int64 { return 0 } +func (x *SupportedModel) GetDisabled() bool { + if x != nil { + return x.Disabled + } + return false +} + +func (x *SupportedModel) GetDisabledReason() string { + if x != nil && x.DisabledReason != nil { + return *x.DisabledReason + } + return "" +} + type ListSupportedModelsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -1108,7 +1234,7 @@ type ListSupportedModelsRequest struct { func (x *ListSupportedModelsRequest) Reset() { *x = ListSupportedModelsRequest{} - mi := &file_chat_v2_chat_proto_msgTypes[18] + mi := &file_chat_v2_chat_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1120,7 +1246,7 @@ func (x *ListSupportedModelsRequest) String() string { func (*ListSupportedModelsRequest) ProtoMessage() {} func (x *ListSupportedModelsRequest) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[18] + mi := &file_chat_v2_chat_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1133,7 +1259,7 @@ func (x *ListSupportedModelsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSupportedModelsRequest.ProtoReflect.Descriptor instead. func (*ListSupportedModelsRequest) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{18} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{19} } type ListSupportedModelsResponse struct { @@ -1145,7 +1271,7 @@ type ListSupportedModelsResponse struct { func (x *ListSupportedModelsResponse) Reset() { *x = ListSupportedModelsResponse{} - mi := &file_chat_v2_chat_proto_msgTypes[19] + mi := &file_chat_v2_chat_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1157,7 +1283,7 @@ func (x *ListSupportedModelsResponse) String() string { func (*ListSupportedModelsResponse) ProtoMessage() {} func (x *ListSupportedModelsResponse) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[19] + mi := &file_chat_v2_chat_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1170,7 +1296,7 @@ func (x *ListSupportedModelsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListSupportedModelsResponse.ProtoReflect.Descriptor instead. func (*ListSupportedModelsResponse) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{19} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{20} } func (x *ListSupportedModelsResponse) GetModels() []*SupportedModel { @@ -1191,7 +1317,7 @@ type StreamInitialization struct { func (x *StreamInitialization) Reset() { *x = StreamInitialization{} - mi := &file_chat_v2_chat_proto_msgTypes[20] + mi := &file_chat_v2_chat_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1203,7 +1329,7 @@ func (x *StreamInitialization) String() string { func (*StreamInitialization) ProtoMessage() {} func (x *StreamInitialization) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[20] + mi := &file_chat_v2_chat_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1216,7 +1342,7 @@ func (x *StreamInitialization) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamInitialization.ProtoReflect.Descriptor instead. func (*StreamInitialization) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{20} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{21} } func (x *StreamInitialization) GetConversationId() string { @@ -1248,7 +1374,7 @@ type StreamPartBegin struct { func (x *StreamPartBegin) Reset() { *x = StreamPartBegin{} - mi := &file_chat_v2_chat_proto_msgTypes[21] + mi := &file_chat_v2_chat_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1260,7 +1386,7 @@ func (x *StreamPartBegin) String() string { func (*StreamPartBegin) ProtoMessage() {} func (x *StreamPartBegin) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[21] + mi := &file_chat_v2_chat_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1273,7 +1399,7 @@ func (x *StreamPartBegin) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamPartBegin.ProtoReflect.Descriptor instead. func (*StreamPartBegin) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{21} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{22} } func (x *StreamPartBegin) GetMessageId() string { @@ -1303,7 +1429,7 @@ type MessageChunk struct { func (x *MessageChunk) Reset() { *x = MessageChunk{} - mi := &file_chat_v2_chat_proto_msgTypes[22] + mi := &file_chat_v2_chat_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1315,7 +1441,7 @@ func (x *MessageChunk) String() string { func (*MessageChunk) ProtoMessage() {} func (x *MessageChunk) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[22] + mi := &file_chat_v2_chat_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1328,7 +1454,7 @@ func (x *MessageChunk) ProtoReflect() protoreflect.Message { // Deprecated: Use MessageChunk.ProtoReflect.Descriptor instead. func (*MessageChunk) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{22} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{23} } func (x *MessageChunk) GetMessageId() string { @@ -1345,6 +1471,58 @@ func (x *MessageChunk) GetDelta() string { return "" } +type ReasoningChunk struct { + state protoimpl.MessageState `protogen:"open.v1"` + MessageId string `protobuf:"bytes,1,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` // The id of the message that this chunk belongs to + Delta string `protobuf:"bytes,2,opt,name=delta,proto3" json:"delta,omitempty"` // The small piece of text + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReasoningChunk) Reset() { + *x = ReasoningChunk{} + mi := &file_chat_v2_chat_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReasoningChunk) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReasoningChunk) ProtoMessage() {} + +func (x *ReasoningChunk) ProtoReflect() protoreflect.Message { + mi := &file_chat_v2_chat_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReasoningChunk.ProtoReflect.Descriptor instead. +func (*ReasoningChunk) Descriptor() ([]byte, []int) { + return file_chat_v2_chat_proto_rawDescGZIP(), []int{24} +} + +func (x *ReasoningChunk) GetMessageId() string { + if x != nil { + return x.MessageId + } + return "" +} + +func (x *ReasoningChunk) GetDelta() string { + if x != nil { + return x.Delta + } + return "" +} + type IncompleteIndicator struct { state protoimpl.MessageState `protogen:"open.v1"` Reason string `protobuf:"bytes,1,opt,name=reason,proto3" json:"reason,omitempty"` @@ -1355,7 +1533,7 @@ type IncompleteIndicator struct { func (x *IncompleteIndicator) Reset() { *x = IncompleteIndicator{} - mi := &file_chat_v2_chat_proto_msgTypes[23] + mi := &file_chat_v2_chat_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1367,7 +1545,7 @@ func (x *IncompleteIndicator) String() string { func (*IncompleteIndicator) ProtoMessage() {} func (x *IncompleteIndicator) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[23] + mi := &file_chat_v2_chat_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1380,7 +1558,7 @@ func (x *IncompleteIndicator) ProtoReflect() protoreflect.Message { // Deprecated: Use IncompleteIndicator.ProtoReflect.Descriptor instead. func (*IncompleteIndicator) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{23} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{25} } func (x *IncompleteIndicator) GetReason() string { @@ -1407,7 +1585,7 @@ type StreamPartEnd struct { func (x *StreamPartEnd) Reset() { *x = StreamPartEnd{} - mi := &file_chat_v2_chat_proto_msgTypes[24] + mi := &file_chat_v2_chat_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1419,7 +1597,7 @@ func (x *StreamPartEnd) String() string { func (*StreamPartEnd) ProtoMessage() {} func (x *StreamPartEnd) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[24] + mi := &file_chat_v2_chat_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1432,7 +1610,7 @@ func (x *StreamPartEnd) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamPartEnd.ProtoReflect.Descriptor instead. func (*StreamPartEnd) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{24} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{26} } func (x *StreamPartEnd) GetMessageId() string { @@ -1459,7 +1637,7 @@ type StreamFinalization struct { func (x *StreamFinalization) Reset() { *x = StreamFinalization{} - mi := &file_chat_v2_chat_proto_msgTypes[25] + mi := &file_chat_v2_chat_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1471,7 +1649,7 @@ func (x *StreamFinalization) String() string { func (*StreamFinalization) ProtoMessage() {} func (x *StreamFinalization) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[25] + mi := &file_chat_v2_chat_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1484,7 +1662,7 @@ func (x *StreamFinalization) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamFinalization.ProtoReflect.Descriptor instead. func (*StreamFinalization) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{25} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{27} } func (x *StreamFinalization) GetConversationId() string { @@ -1503,7 +1681,7 @@ type StreamError struct { func (x *StreamError) Reset() { *x = StreamError{} - mi := &file_chat_v2_chat_proto_msgTypes[26] + mi := &file_chat_v2_chat_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1515,7 +1693,7 @@ func (x *StreamError) String() string { func (*StreamError) ProtoMessage() {} func (x *StreamError) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[26] + mi := &file_chat_v2_chat_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1528,7 +1706,7 @@ func (x *StreamError) ProtoReflect() protoreflect.Message { // Deprecated: Use StreamError.ProtoReflect.Descriptor instead. func (*StreamError) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{26} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{28} } func (x *StreamError) GetErrorMessage() string { @@ -1551,13 +1729,16 @@ type CreateConversationMessageStreamRequest struct { UserSelectedText *string `protobuf:"bytes,5,opt,name=user_selected_text,json=userSelectedText,proto3,oneof" json:"user_selected_text,omitempty"` ConversationType *ConversationType `protobuf:"varint,6,opt,name=conversation_type,json=conversationType,proto3,enum=chat.v2.ConversationType,oneof" json:"conversation_type,omitempty"` Surrounding *string `protobuf:"bytes,8,opt,name=surrounding,proto3,oneof" json:"surrounding,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + ParentMessageId *string `protobuf:"bytes,9,opt,name=parent_message_id,json=parentMessageId,proto3,oneof" json:"parent_message_id,omitempty"` + // Overleaf authentication for tools that need direct Overleaf API access + OverleafAuth *v1.OverleafAuth `protobuf:"bytes,10,opt,name=overleaf_auth,json=overleafAuth,proto3,oneof" json:"overleaf_auth,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CreateConversationMessageStreamRequest) Reset() { *x = CreateConversationMessageStreamRequest{} - mi := &file_chat_v2_chat_proto_msgTypes[27] + mi := &file_chat_v2_chat_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1569,7 +1750,7 @@ func (x *CreateConversationMessageStreamRequest) String() string { func (*CreateConversationMessageStreamRequest) ProtoMessage() {} func (x *CreateConversationMessageStreamRequest) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[27] + mi := &file_chat_v2_chat_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1582,7 +1763,7 @@ func (x *CreateConversationMessageStreamRequest) ProtoReflect() protoreflect.Mes // Deprecated: Use CreateConversationMessageStreamRequest.ProtoReflect.Descriptor instead. func (*CreateConversationMessageStreamRequest) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{27} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{29} } func (x *CreateConversationMessageStreamRequest) GetProjectId() string { @@ -1634,6 +1815,20 @@ func (x *CreateConversationMessageStreamRequest) GetSurrounding() string { return "" } +func (x *CreateConversationMessageStreamRequest) GetParentMessageId() string { + if x != nil && x.ParentMessageId != nil { + return *x.ParentMessageId + } + return "" +} + +func (x *CreateConversationMessageStreamRequest) GetOverleafAuth() *v1.OverleafAuth { + if x != nil { + return x.OverleafAuth + } + return nil +} + // Response for streaming a message within an existing conversation type CreateConversationMessageStreamResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1646,6 +1841,7 @@ type CreateConversationMessageStreamResponse struct { // *CreateConversationMessageStreamResponse_StreamPartEnd // *CreateConversationMessageStreamResponse_StreamFinalization // *CreateConversationMessageStreamResponse_StreamError + // *CreateConversationMessageStreamResponse_ReasoningChunk ResponsePayload isCreateConversationMessageStreamResponse_ResponsePayload `protobuf_oneof:"response_payload"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1653,7 +1849,7 @@ type CreateConversationMessageStreamResponse struct { func (x *CreateConversationMessageStreamResponse) Reset() { *x = CreateConversationMessageStreamResponse{} - mi := &file_chat_v2_chat_proto_msgTypes[28] + mi := &file_chat_v2_chat_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1665,7 +1861,7 @@ func (x *CreateConversationMessageStreamResponse) String() string { func (*CreateConversationMessageStreamResponse) ProtoMessage() {} func (x *CreateConversationMessageStreamResponse) ProtoReflect() protoreflect.Message { - mi := &file_chat_v2_chat_proto_msgTypes[28] + mi := &file_chat_v2_chat_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1678,7 +1874,7 @@ func (x *CreateConversationMessageStreamResponse) ProtoReflect() protoreflect.Me // Deprecated: Use CreateConversationMessageStreamResponse.ProtoReflect.Descriptor instead. func (*CreateConversationMessageStreamResponse) Descriptor() ([]byte, []int) { - return file_chat_v2_chat_proto_rawDescGZIP(), []int{28} + return file_chat_v2_chat_proto_rawDescGZIP(), []int{30} } func (x *CreateConversationMessageStreamResponse) GetResponsePayload() isCreateConversationMessageStreamResponse_ResponsePayload { @@ -1751,6 +1947,15 @@ func (x *CreateConversationMessageStreamResponse) GetStreamError() *StreamError return nil } +func (x *CreateConversationMessageStreamResponse) GetReasoningChunk() *ReasoningChunk { + if x != nil { + if x, ok := x.ResponsePayload.(*CreateConversationMessageStreamResponse_ReasoningChunk); ok { + return x.ReasoningChunk + } + } + return nil +} + type isCreateConversationMessageStreamResponse_ResponsePayload interface { isCreateConversationMessageStreamResponse_ResponsePayload() } @@ -1783,6 +1988,10 @@ type CreateConversationMessageStreamResponse_StreamError struct { StreamError *StreamError `protobuf:"bytes,7,opt,name=stream_error,json=streamError,proto3,oneof"` } +type CreateConversationMessageStreamResponse_ReasoningChunk struct { + ReasoningChunk *ReasoningChunk `protobuf:"bytes,8,opt,name=reasoning_chunk,json=reasoningChunk,proto3,oneof"` +} + func (*CreateConversationMessageStreamResponse_StreamInitialization) isCreateConversationMessageStreamResponse_ResponsePayload() { } @@ -1804,11 +2013,14 @@ func (*CreateConversationMessageStreamResponse_StreamFinalization) isCreateConve func (*CreateConversationMessageStreamResponse_StreamError) isCreateConversationMessageStreamResponse_ResponsePayload() { } +func (*CreateConversationMessageStreamResponse_ReasoningChunk) isCreateConversationMessageStreamResponse_ResponsePayload() { +} + var File_chat_v2_chat_proto protoreflect.FileDescriptor const file_chat_v2_chat_proto_rawDesc = "" + "\n" + - "\x12chat/v2/chat.proto\x12\achat.v2\x1a\x1cgoogle/api/annotations.proto\"k\n" + + "\x12chat/v2/chat.proto\x12\achat.v2\x1a\x1cgoogle/api/annotations.proto\x1a\x16shared/v1/shared.proto\"k\n" + "\x13MessageTypeToolCall\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + "\x04args\x18\x02 \x01(\tR\x04args\x12\x16\n" + @@ -1818,11 +2030,14 @@ const file_chat_v2_chat_proto_rawDesc = "" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + "\x04args\x18\x02 \x01(\tR\x04args\"-\n" + "\x11MessageTypeSystem\x12\x18\n" + - "\acontent\x18\x01 \x01(\tR\acontent\"O\n" + + "\acontent\x18\x01 \x01(\tR\acontent\"\x80\x01\n" + "\x14MessageTypeAssistant\x12\x18\n" + "\acontent\x18\x01 \x01(\tR\acontent\x12\x1d\n" + "\n" + - "model_slug\x18\x02 \x01(\tR\tmodelSlug\"\x9e\x01\n" + + "model_slug\x18\x02 \x01(\tR\tmodelSlug\x12!\n" + + "\treasoning\x18\x03 \x01(\tH\x00R\treasoning\x88\x01\x01B\f\n" + + "\n" + + "_reasoning\"\x9e\x01\n" + "\x0fMessageTypeUser\x12\x18\n" + "\acontent\x18\x01 \x01(\tR\acontent\x12(\n" + "\rselected_text\x18\x02 \x01(\tH\x00R\fselectedText\x88\x01\x01\x12%\n" + @@ -1843,21 +2058,35 @@ const file_chat_v2_chat_proto_rawDesc = "" + "\n" + "message_id\x18\x01 \x01(\tR\tmessageId\x121\n" + "\apayload\x18\x02 \x01(\v2\x17.chat.v2.MessagePayloadR\apayload\x12\x1c\n" + - "\ttimestamp\x18\x03 \x01(\x03R\ttimestamp\"\x81\x01\n" + + "\ttimestamp\x18\x03 \x01(\x03R\ttimestamp\"Z\n" + + "\n" + + "BranchInfo\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n" + + "\n" + + "created_at\x18\x02 \x01(\x03R\tcreatedAt\x12\x1d\n" + + "\n" + + "updated_at\x18\x03 \x01(\x03R\tupdatedAt\"\xb7\x02\n" + "\fConversation\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + "\x05title\x18\x02 \x01(\tR\x05title\x12\x1d\n" + "\n" + "model_slug\x18\x03 \x01(\tR\tmodelSlug\x12,\n" + - "\bmessages\x18\x04 \x03(\v2\x10.chat.v2.MessageR\bmessages\"M\n" + + "\bmessages\x18\x04 \x03(\v2\x10.chat.v2.MessageR\bmessages\x12*\n" + + "\x11current_branch_id\x18\x05 \x01(\tR\x0fcurrentBranchId\x12/\n" + + "\bbranches\x18\x06 \x03(\v2\x13.chat.v2.BranchInfoR\bbranches\x120\n" + + "\x14current_branch_index\x18\a \x01(\x05R\x12currentBranchIndex\x12%\n" + + "\x0etotal_branches\x18\b \x01(\x05R\rtotalBranches\"M\n" + "\x18ListConversationsRequest\x12\"\n" + "\n" + "project_id\x18\x01 \x01(\tH\x00R\tprojectId\x88\x01\x01B\r\n" + "\v_project_id\"X\n" + "\x19ListConversationsResponse\x12;\n" + - "\rconversations\x18\x01 \x03(\v2\x15.chat.v2.ConversationR\rconversations\"A\n" + + "\rconversations\x18\x01 \x03(\v2\x15.chat.v2.ConversationR\rconversations\"q\n" + "\x16GetConversationRequest\x12'\n" + - "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\"T\n" + + "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\x12 \n" + + "\tbranch_id\x18\x02 \x01(\tH\x00R\bbranchId\x88\x01\x01B\f\n" + + "\n" + + "_branch_id\"T\n" + "\x17GetConversationResponse\x129\n" + "\fconversation\x18\x01 \x01(\v2\x15.chat.v2.ConversationR\fconversation\"Z\n" + "\x19UpdateConversationRequest\x12'\n" + @@ -1867,7 +2096,7 @@ const file_chat_v2_chat_proto_rawDesc = "" + "\fconversation\x18\x01 \x01(\v2\x15.chat.v2.ConversationR\fconversation\"D\n" + "\x19DeleteConversationRequest\x12'\n" + "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\"\x1c\n" + - "\x1aDeleteConversationResponse\"\xc0\x01\n" + + "\x1aDeleteConversationResponse\"\x9e\x02\n" + "\x0eSupportedModel\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + "\x04slug\x18\x02 \x01(\tR\x04slug\x12#\n" + @@ -1876,7 +2105,10 @@ const file_chat_v2_chat_proto_rawDesc = "" + "max_output\x18\x04 \x01(\x03R\tmaxOutput\x12\x1f\n" + "\vinput_price\x18\x05 \x01(\x03R\n" + "inputPrice\x12!\n" + - "\foutput_price\x18\x06 \x01(\x03R\voutputPrice\"\x1c\n" + + "\foutput_price\x18\x06 \x01(\x03R\voutputPrice\x12\x1a\n" + + "\bdisabled\x18\a \x01(\bR\bdisabled\x12,\n" + + "\x0fdisabled_reason\x18\b \x01(\tH\x00R\x0edisabledReason\x88\x01\x01B\x12\n" + + "\x10_disabled_reason\"\x1c\n" + "\x1aListSupportedModelsRequest\"N\n" + "\x1bListSupportedModelsResponse\x12/\n" + "\x06models\x18\x01 \x03(\v2\x17.chat.v2.SupportedModelR\x06models\"^\n" + @@ -1891,6 +2123,10 @@ const file_chat_v2_chat_proto_rawDesc = "" + "\fMessageChunk\x12\x1d\n" + "\n" + "message_id\x18\x01 \x01(\tR\tmessageId\x12\x14\n" + + "\x05delta\x18\x02 \x01(\tR\x05delta\"E\n" + + "\x0eReasoningChunk\x12\x1d\n" + + "\n" + + "message_id\x18\x01 \x01(\tR\tmessageId\x12\x14\n" + "\x05delta\x18\x02 \x01(\tR\x05delta\"N\n" + "\x13IncompleteIndicator\x12\x16\n" + "\x06reason\x18\x01 \x01(\tR\x06reason\x12\x1f\n" + @@ -1903,7 +2139,7 @@ const file_chat_v2_chat_proto_rawDesc = "" + "\x12StreamFinalization\x12'\n" + "\x0fconversation_id\x18\x01 \x01(\tR\x0econversationId\"2\n" + "\vStreamError\x12#\n" + - "\rerror_message\x18\x01 \x01(\tR\ferrorMessage\"\xaf\x03\n" + + "\rerror_message\x18\x01 \x01(\tR\ferrorMessage\"\xcb\x04\n" + "&CreateConversationMessageStreamRequest\x12\x1d\n" + "\n" + "project_id\x18\x01 \x01(\tR\tprojectId\x12,\n" + @@ -1913,11 +2149,16 @@ const file_chat_v2_chat_proto_rawDesc = "" + "\fuser_message\x18\x04 \x01(\tR\vuserMessage\x121\n" + "\x12user_selected_text\x18\x05 \x01(\tH\x01R\x10userSelectedText\x88\x01\x01\x12K\n" + "\x11conversation_type\x18\x06 \x01(\x0e2\x19.chat.v2.ConversationTypeH\x02R\x10conversationType\x88\x01\x01\x12%\n" + - "\vsurrounding\x18\b \x01(\tH\x03R\vsurrounding\x88\x01\x01B\x12\n" + + "\vsurrounding\x18\b \x01(\tH\x03R\vsurrounding\x88\x01\x01\x12/\n" + + "\x11parent_message_id\x18\t \x01(\tH\x04R\x0fparentMessageId\x88\x01\x01\x12A\n" + + "\roverleaf_auth\x18\n" + + " \x01(\v2\x17.shared.v1.OverleafAuthH\x05R\foverleafAuth\x88\x01\x01B\x12\n" + "\x10_conversation_idB\x15\n" + "\x13_user_selected_textB\x14\n" + "\x12_conversation_typeB\x0e\n" + - "\f_surrounding\"\xb9\x04\n" + + "\f_surroundingB\x14\n" + + "\x12_parent_message_idB\x10\n" + + "\x0e_overleaf_auth\"\xfd\x04\n" + "'CreateConversationMessageStreamResponse\x12T\n" + "\x15stream_initialization\x18\x01 \x01(\v2\x1d.chat.v2.StreamInitializationH\x00R\x14streamInitialization\x12F\n" + "\x11stream_part_begin\x18\x02 \x01(\v2\x18.chat.v2.StreamPartBeginH\x00R\x0fstreamPartBegin\x12<\n" + @@ -1925,7 +2166,8 @@ const file_chat_v2_chat_proto_rawDesc = "" + "\x14incomplete_indicator\x18\x04 \x01(\v2\x1c.chat.v2.IncompleteIndicatorH\x00R\x13incompleteIndicator\x12@\n" + "\x0fstream_part_end\x18\x05 \x01(\v2\x16.chat.v2.StreamPartEndH\x00R\rstreamPartEnd\x12N\n" + "\x13stream_finalization\x18\x06 \x01(\v2\x1b.chat.v2.StreamFinalizationH\x00R\x12streamFinalization\x129\n" + - "\fstream_error\x18\a \x01(\v2\x14.chat.v2.StreamErrorH\x00R\vstreamErrorB\x12\n" + + "\fstream_error\x18\a \x01(\v2\x14.chat.v2.StreamErrorH\x00R\vstreamError\x12B\n" + + "\x0freasoning_chunk\x18\b \x01(\v2\x17.chat.v2.ReasoningChunkH\x00R\x0ereasoningChunkB\x12\n" + "\x10response_payload*R\n" + "\x10ConversationType\x12!\n" + "\x1dCONVERSATION_TYPE_UNSPECIFIED\x10\x00\x12\x1b\n" + @@ -1952,7 +2194,7 @@ func file_chat_v2_chat_proto_rawDescGZIP() []byte { } var file_chat_v2_chat_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_chat_v2_chat_proto_msgTypes = make([]protoimpl.MessageInfo, 29) +var file_chat_v2_chat_proto_msgTypes = make([]protoimpl.MessageInfo, 31) var file_chat_v2_chat_proto_goTypes = []any{ (ConversationType)(0), // 0: chat.v2.ConversationType (*MessageTypeToolCall)(nil), // 1: chat.v2.MessageTypeToolCall @@ -1963,27 +2205,30 @@ var file_chat_v2_chat_proto_goTypes = []any{ (*MessageTypeUnknown)(nil), // 6: chat.v2.MessageTypeUnknown (*MessagePayload)(nil), // 7: chat.v2.MessagePayload (*Message)(nil), // 8: chat.v2.Message - (*Conversation)(nil), // 9: chat.v2.Conversation - (*ListConversationsRequest)(nil), // 10: chat.v2.ListConversationsRequest - (*ListConversationsResponse)(nil), // 11: chat.v2.ListConversationsResponse - (*GetConversationRequest)(nil), // 12: chat.v2.GetConversationRequest - (*GetConversationResponse)(nil), // 13: chat.v2.GetConversationResponse - (*UpdateConversationRequest)(nil), // 14: chat.v2.UpdateConversationRequest - (*UpdateConversationResponse)(nil), // 15: chat.v2.UpdateConversationResponse - (*DeleteConversationRequest)(nil), // 16: chat.v2.DeleteConversationRequest - (*DeleteConversationResponse)(nil), // 17: chat.v2.DeleteConversationResponse - (*SupportedModel)(nil), // 18: chat.v2.SupportedModel - (*ListSupportedModelsRequest)(nil), // 19: chat.v2.ListSupportedModelsRequest - (*ListSupportedModelsResponse)(nil), // 20: chat.v2.ListSupportedModelsResponse - (*StreamInitialization)(nil), // 21: chat.v2.StreamInitialization - (*StreamPartBegin)(nil), // 22: chat.v2.StreamPartBegin - (*MessageChunk)(nil), // 23: chat.v2.MessageChunk - (*IncompleteIndicator)(nil), // 24: chat.v2.IncompleteIndicator - (*StreamPartEnd)(nil), // 25: chat.v2.StreamPartEnd - (*StreamFinalization)(nil), // 26: chat.v2.StreamFinalization - (*StreamError)(nil), // 27: chat.v2.StreamError - (*CreateConversationMessageStreamRequest)(nil), // 28: chat.v2.CreateConversationMessageStreamRequest - (*CreateConversationMessageStreamResponse)(nil), // 29: chat.v2.CreateConversationMessageStreamResponse + (*BranchInfo)(nil), // 9: chat.v2.BranchInfo + (*Conversation)(nil), // 10: chat.v2.Conversation + (*ListConversationsRequest)(nil), // 11: chat.v2.ListConversationsRequest + (*ListConversationsResponse)(nil), // 12: chat.v2.ListConversationsResponse + (*GetConversationRequest)(nil), // 13: chat.v2.GetConversationRequest + (*GetConversationResponse)(nil), // 14: chat.v2.GetConversationResponse + (*UpdateConversationRequest)(nil), // 15: chat.v2.UpdateConversationRequest + (*UpdateConversationResponse)(nil), // 16: chat.v2.UpdateConversationResponse + (*DeleteConversationRequest)(nil), // 17: chat.v2.DeleteConversationRequest + (*DeleteConversationResponse)(nil), // 18: chat.v2.DeleteConversationResponse + (*SupportedModel)(nil), // 19: chat.v2.SupportedModel + (*ListSupportedModelsRequest)(nil), // 20: chat.v2.ListSupportedModelsRequest + (*ListSupportedModelsResponse)(nil), // 21: chat.v2.ListSupportedModelsResponse + (*StreamInitialization)(nil), // 22: chat.v2.StreamInitialization + (*StreamPartBegin)(nil), // 23: chat.v2.StreamPartBegin + (*MessageChunk)(nil), // 24: chat.v2.MessageChunk + (*ReasoningChunk)(nil), // 25: chat.v2.ReasoningChunk + (*IncompleteIndicator)(nil), // 26: chat.v2.IncompleteIndicator + (*StreamPartEnd)(nil), // 27: chat.v2.StreamPartEnd + (*StreamFinalization)(nil), // 28: chat.v2.StreamFinalization + (*StreamError)(nil), // 29: chat.v2.StreamError + (*CreateConversationMessageStreamRequest)(nil), // 30: chat.v2.CreateConversationMessageStreamRequest + (*CreateConversationMessageStreamResponse)(nil), // 31: chat.v2.CreateConversationMessageStreamResponse + (*v1.OverleafAuth)(nil), // 32: shared.v1.OverleafAuth } var file_chat_v2_chat_proto_depIdxs = []int32{ 3, // 0: chat.v2.MessagePayload.system:type_name -> chat.v2.MessageTypeSystem @@ -1994,37 +2239,40 @@ var file_chat_v2_chat_proto_depIdxs = []int32{ 6, // 5: chat.v2.MessagePayload.unknown:type_name -> chat.v2.MessageTypeUnknown 7, // 6: chat.v2.Message.payload:type_name -> chat.v2.MessagePayload 8, // 7: chat.v2.Conversation.messages:type_name -> chat.v2.Message - 9, // 8: chat.v2.ListConversationsResponse.conversations:type_name -> chat.v2.Conversation - 9, // 9: chat.v2.GetConversationResponse.conversation:type_name -> chat.v2.Conversation - 9, // 10: chat.v2.UpdateConversationResponse.conversation:type_name -> chat.v2.Conversation - 18, // 11: chat.v2.ListSupportedModelsResponse.models:type_name -> chat.v2.SupportedModel - 7, // 12: chat.v2.StreamPartBegin.payload:type_name -> chat.v2.MessagePayload - 7, // 13: chat.v2.StreamPartEnd.payload:type_name -> chat.v2.MessagePayload - 0, // 14: chat.v2.CreateConversationMessageStreamRequest.conversation_type:type_name -> chat.v2.ConversationType - 21, // 15: chat.v2.CreateConversationMessageStreamResponse.stream_initialization:type_name -> chat.v2.StreamInitialization - 22, // 16: chat.v2.CreateConversationMessageStreamResponse.stream_part_begin:type_name -> chat.v2.StreamPartBegin - 23, // 17: chat.v2.CreateConversationMessageStreamResponse.message_chunk:type_name -> chat.v2.MessageChunk - 24, // 18: chat.v2.CreateConversationMessageStreamResponse.incomplete_indicator:type_name -> chat.v2.IncompleteIndicator - 25, // 19: chat.v2.CreateConversationMessageStreamResponse.stream_part_end:type_name -> chat.v2.StreamPartEnd - 26, // 20: chat.v2.CreateConversationMessageStreamResponse.stream_finalization:type_name -> chat.v2.StreamFinalization - 27, // 21: chat.v2.CreateConversationMessageStreamResponse.stream_error:type_name -> chat.v2.StreamError - 10, // 22: chat.v2.ChatService.ListConversations:input_type -> chat.v2.ListConversationsRequest - 12, // 23: chat.v2.ChatService.GetConversation:input_type -> chat.v2.GetConversationRequest - 28, // 24: chat.v2.ChatService.CreateConversationMessageStream:input_type -> chat.v2.CreateConversationMessageStreamRequest - 14, // 25: chat.v2.ChatService.UpdateConversation:input_type -> chat.v2.UpdateConversationRequest - 16, // 26: chat.v2.ChatService.DeleteConversation:input_type -> chat.v2.DeleteConversationRequest - 19, // 27: chat.v2.ChatService.ListSupportedModels:input_type -> chat.v2.ListSupportedModelsRequest - 11, // 28: chat.v2.ChatService.ListConversations:output_type -> chat.v2.ListConversationsResponse - 13, // 29: chat.v2.ChatService.GetConversation:output_type -> chat.v2.GetConversationResponse - 29, // 30: chat.v2.ChatService.CreateConversationMessageStream:output_type -> chat.v2.CreateConversationMessageStreamResponse - 15, // 31: chat.v2.ChatService.UpdateConversation:output_type -> chat.v2.UpdateConversationResponse - 17, // 32: chat.v2.ChatService.DeleteConversation:output_type -> chat.v2.DeleteConversationResponse - 20, // 33: chat.v2.ChatService.ListSupportedModels:output_type -> chat.v2.ListSupportedModelsResponse - 28, // [28:34] is the sub-list for method output_type - 22, // [22:28] is the sub-list for method input_type - 22, // [22:22] is the sub-list for extension type_name - 22, // [22:22] is the sub-list for extension extendee - 0, // [0:22] is the sub-list for field type_name + 9, // 8: chat.v2.Conversation.branches:type_name -> chat.v2.BranchInfo + 10, // 9: chat.v2.ListConversationsResponse.conversations:type_name -> chat.v2.Conversation + 10, // 10: chat.v2.GetConversationResponse.conversation:type_name -> chat.v2.Conversation + 10, // 11: chat.v2.UpdateConversationResponse.conversation:type_name -> chat.v2.Conversation + 19, // 12: chat.v2.ListSupportedModelsResponse.models:type_name -> chat.v2.SupportedModel + 7, // 13: chat.v2.StreamPartBegin.payload:type_name -> chat.v2.MessagePayload + 7, // 14: chat.v2.StreamPartEnd.payload:type_name -> chat.v2.MessagePayload + 0, // 15: chat.v2.CreateConversationMessageStreamRequest.conversation_type:type_name -> chat.v2.ConversationType + 32, // 16: chat.v2.CreateConversationMessageStreamRequest.overleaf_auth:type_name -> shared.v1.OverleafAuth + 22, // 17: chat.v2.CreateConversationMessageStreamResponse.stream_initialization:type_name -> chat.v2.StreamInitialization + 23, // 18: chat.v2.CreateConversationMessageStreamResponse.stream_part_begin:type_name -> chat.v2.StreamPartBegin + 24, // 19: chat.v2.CreateConversationMessageStreamResponse.message_chunk:type_name -> chat.v2.MessageChunk + 26, // 20: chat.v2.CreateConversationMessageStreamResponse.incomplete_indicator:type_name -> chat.v2.IncompleteIndicator + 27, // 21: chat.v2.CreateConversationMessageStreamResponse.stream_part_end:type_name -> chat.v2.StreamPartEnd + 28, // 22: chat.v2.CreateConversationMessageStreamResponse.stream_finalization:type_name -> chat.v2.StreamFinalization + 29, // 23: chat.v2.CreateConversationMessageStreamResponse.stream_error:type_name -> chat.v2.StreamError + 25, // 24: chat.v2.CreateConversationMessageStreamResponse.reasoning_chunk:type_name -> chat.v2.ReasoningChunk + 11, // 25: chat.v2.ChatService.ListConversations:input_type -> chat.v2.ListConversationsRequest + 13, // 26: chat.v2.ChatService.GetConversation:input_type -> chat.v2.GetConversationRequest + 30, // 27: chat.v2.ChatService.CreateConversationMessageStream:input_type -> chat.v2.CreateConversationMessageStreamRequest + 15, // 28: chat.v2.ChatService.UpdateConversation:input_type -> chat.v2.UpdateConversationRequest + 17, // 29: chat.v2.ChatService.DeleteConversation:input_type -> chat.v2.DeleteConversationRequest + 20, // 30: chat.v2.ChatService.ListSupportedModels:input_type -> chat.v2.ListSupportedModelsRequest + 12, // 31: chat.v2.ChatService.ListConversations:output_type -> chat.v2.ListConversationsResponse + 14, // 32: chat.v2.ChatService.GetConversation:output_type -> chat.v2.GetConversationResponse + 31, // 33: chat.v2.ChatService.CreateConversationMessageStream:output_type -> chat.v2.CreateConversationMessageStreamResponse + 16, // 34: chat.v2.ChatService.UpdateConversation:output_type -> chat.v2.UpdateConversationResponse + 18, // 35: chat.v2.ChatService.DeleteConversation:output_type -> chat.v2.DeleteConversationResponse + 21, // 36: chat.v2.ChatService.ListSupportedModels:output_type -> chat.v2.ListSupportedModelsResponse + 31, // [31:37] is the sub-list for method output_type + 25, // [25:31] is the sub-list for method input_type + 25, // [25:25] is the sub-list for extension type_name + 25, // [25:25] is the sub-list for extension extendee + 0, // [0:25] is the sub-list for field type_name } func init() { file_chat_v2_chat_proto_init() } @@ -2032,6 +2280,7 @@ func file_chat_v2_chat_proto_init() { if File_chat_v2_chat_proto != nil { return } + file_chat_v2_chat_proto_msgTypes[3].OneofWrappers = []any{} file_chat_v2_chat_proto_msgTypes[4].OneofWrappers = []any{} file_chat_v2_chat_proto_msgTypes[6].OneofWrappers = []any{ (*MessagePayload_System)(nil), @@ -2041,9 +2290,11 @@ func file_chat_v2_chat_proto_init() { (*MessagePayload_ToolCall)(nil), (*MessagePayload_Unknown)(nil), } - file_chat_v2_chat_proto_msgTypes[9].OneofWrappers = []any{} - file_chat_v2_chat_proto_msgTypes[27].OneofWrappers = []any{} - file_chat_v2_chat_proto_msgTypes[28].OneofWrappers = []any{ + file_chat_v2_chat_proto_msgTypes[10].OneofWrappers = []any{} + file_chat_v2_chat_proto_msgTypes[12].OneofWrappers = []any{} + file_chat_v2_chat_proto_msgTypes[18].OneofWrappers = []any{} + file_chat_v2_chat_proto_msgTypes[29].OneofWrappers = []any{} + file_chat_v2_chat_proto_msgTypes[30].OneofWrappers = []any{ (*CreateConversationMessageStreamResponse_StreamInitialization)(nil), (*CreateConversationMessageStreamResponse_StreamPartBegin)(nil), (*CreateConversationMessageStreamResponse_MessageChunk)(nil), @@ -2051,6 +2302,7 @@ func file_chat_v2_chat_proto_init() { (*CreateConversationMessageStreamResponse_StreamPartEnd)(nil), (*CreateConversationMessageStreamResponse_StreamFinalization)(nil), (*CreateConversationMessageStreamResponse_StreamError)(nil), + (*CreateConversationMessageStreamResponse_ReasoningChunk)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -2058,7 +2310,7 @@ func file_chat_v2_chat_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_chat_v2_chat_proto_rawDesc), len(file_chat_v2_chat_proto_rawDesc)), NumEnums: 1, - NumMessages: 29, + NumMessages: 31, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/gen/api/chat/v2/chat.pb.gw.go b/pkg/gen/api/chat/v2/chat.pb.gw.go index 81f7e4e6..7165d02a 100644 --- a/pkg/gen/api/chat/v2/chat.pb.gw.go +++ b/pkg/gen/api/chat/v2/chat.pb.gw.go @@ -70,6 +70,8 @@ func local_request_ChatService_ListConversations_0(ctx context.Context, marshale return msg, metadata, err } +var filter_ChatService_GetConversation_0 = &utilities.DoubleArray{Encoding: map[string]int{"conversation_id": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} + func request_ChatService_GetConversation_0(ctx context.Context, marshaler runtime.Marshaler, client ChatServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq GetConversationRequest @@ -87,6 +89,12 @@ func request_ChatService_GetConversation_0(ctx context.Context, marshaler runtim if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ChatService_GetConversation_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } msg, err := client.GetConversation(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err } @@ -105,6 +113,12 @@ func local_request_ChatService_GetConversation_0(ctx context.Context, marshaler if err != nil { return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "conversation_id", err) } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ChatService_GetConversation_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } msg, err := server.GetConversation(ctx, &protoReq) return msg, metadata, err } diff --git a/pkg/gen/api/project/v2/project.pb.go b/pkg/gen/api/project/v2/project.pb.go new file mode 100644 index 00000000..3dd8ca11 --- /dev/null +++ b/pkg/gen/api/project/v2/project.pb.go @@ -0,0 +1,592 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: project/v2/project.proto + +package projectv2 + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Project struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` + RootDocId string `protobuf:"bytes,5,opt,name=root_doc_id,json=rootDocId,proto3" json:"root_doc_id,omitempty"` + RootFolder *ProjectFolder `protobuf:"bytes,6,opt,name=root_folder,json=rootFolder,proto3" json:"root_folder,omitempty"` + Instructions string `protobuf:"bytes,7,opt,name=instructions,proto3" json:"instructions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Project) Reset() { + *x = Project{} + mi := &file_project_v2_project_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Project) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Project) ProtoMessage() {} + +func (x *Project) ProtoReflect() protoreflect.Message { + mi := &file_project_v2_project_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Project.ProtoReflect.Descriptor instead. +func (*Project) Descriptor() ([]byte, []int) { + return file_project_v2_project_proto_rawDescGZIP(), []int{0} +} + +func (x *Project) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Project) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *Project) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +func (x *Project) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Project) GetRootDocId() string { + if x != nil { + return x.RootDocId + } + return "" +} + +func (x *Project) GetRootFolder() *ProjectFolder { + if x != nil { + return x.RootFolder + } + return nil +} + +func (x *Project) GetInstructions() string { + if x != nil { + return x.Instructions + } + return "" +} + +type ProjectFolder struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Docs []*ProjectDoc `protobuf:"bytes,3,rep,name=docs,proto3" json:"docs,omitempty"` + Folders []*ProjectFolder `protobuf:"bytes,4,rep,name=folders,proto3" json:"folders,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProjectFolder) Reset() { + *x = ProjectFolder{} + mi := &file_project_v2_project_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProjectFolder) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProjectFolder) ProtoMessage() {} + +func (x *ProjectFolder) ProtoReflect() protoreflect.Message { + mi := &file_project_v2_project_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProjectFolder.ProtoReflect.Descriptor instead. +func (*ProjectFolder) Descriptor() ([]byte, []int) { + return file_project_v2_project_proto_rawDescGZIP(), []int{1} +} + +func (x *ProjectFolder) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ProjectFolder) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ProjectFolder) GetDocs() []*ProjectDoc { + if x != nil { + return x.Docs + } + return nil +} + +func (x *ProjectFolder) GetFolders() []*ProjectFolder { + if x != nil { + return x.Folders + } + return nil +} + +type ProjectDoc struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Version int32 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` + Filename string `protobuf:"bytes,3,opt,name=filename,proto3" json:"filename,omitempty"` + Filepath string `protobuf:"bytes,4,opt,name=filepath,proto3" json:"filepath,omitempty"` + Lines []string `protobuf:"bytes,5,rep,name=lines,proto3" json:"lines,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProjectDoc) Reset() { + *x = ProjectDoc{} + mi := &file_project_v2_project_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProjectDoc) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProjectDoc) ProtoMessage() {} + +func (x *ProjectDoc) ProtoReflect() protoreflect.Message { + mi := &file_project_v2_project_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProjectDoc.ProtoReflect.Descriptor instead. +func (*ProjectDoc) Descriptor() ([]byte, []int) { + return file_project_v2_project_proto_rawDescGZIP(), []int{2} +} + +func (x *ProjectDoc) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ProjectDoc) GetVersion() int32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *ProjectDoc) GetFilename() string { + if x != nil { + return x.Filename + } + return "" +} + +func (x *ProjectDoc) GetFilepath() string { + if x != nil { + return x.Filepath + } + return "" +} + +func (x *ProjectDoc) GetLines() []string { + if x != nil { + return x.Lines + } + return nil +} + +type UpsertProjectRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ProjectId string `protobuf:"bytes,1,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + Name *string `protobuf:"bytes,2,opt,name=name,proto3,oneof" json:"name,omitempty"` + RootDocId *string `protobuf:"bytes,3,opt,name=root_doc_id,json=rootDocId,proto3,oneof" json:"root_doc_id,omitempty"` + RootFolder *ProjectFolder `protobuf:"bytes,4,opt,name=root_folder,json=rootFolder,proto3,oneof" json:"root_folder,omitempty"` + Instructions *string `protobuf:"bytes,5,opt,name=instructions,proto3,oneof" json:"instructions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpsertProjectRequest) Reset() { + *x = UpsertProjectRequest{} + mi := &file_project_v2_project_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpsertProjectRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpsertProjectRequest) ProtoMessage() {} + +func (x *UpsertProjectRequest) ProtoReflect() protoreflect.Message { + mi := &file_project_v2_project_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpsertProjectRequest.ProtoReflect.Descriptor instead. +func (*UpsertProjectRequest) Descriptor() ([]byte, []int) { + return file_project_v2_project_proto_rawDescGZIP(), []int{3} +} + +func (x *UpsertProjectRequest) GetProjectId() string { + if x != nil { + return x.ProjectId + } + return "" +} + +func (x *UpsertProjectRequest) GetName() string { + if x != nil && x.Name != nil { + return *x.Name + } + return "" +} + +func (x *UpsertProjectRequest) GetRootDocId() string { + if x != nil && x.RootDocId != nil { + return *x.RootDocId + } + return "" +} + +func (x *UpsertProjectRequest) GetRootFolder() *ProjectFolder { + if x != nil { + return x.RootFolder + } + return nil +} + +func (x *UpsertProjectRequest) GetInstructions() string { + if x != nil && x.Instructions != nil { + return *x.Instructions + } + return "" +} + +type UpsertProjectResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Project *Project `protobuf:"bytes,1,opt,name=project,proto3" json:"project,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpsertProjectResponse) Reset() { + *x = UpsertProjectResponse{} + mi := &file_project_v2_project_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpsertProjectResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpsertProjectResponse) ProtoMessage() {} + +func (x *UpsertProjectResponse) ProtoReflect() protoreflect.Message { + mi := &file_project_v2_project_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpsertProjectResponse.ProtoReflect.Descriptor instead. +func (*UpsertProjectResponse) Descriptor() ([]byte, []int) { + return file_project_v2_project_proto_rawDescGZIP(), []int{4} +} + +func (x *UpsertProjectResponse) GetProject() *Project { + if x != nil { + return x.Project + } + return nil +} + +type GetProjectRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + ProjectId string `protobuf:"bytes,1,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProjectRequest) Reset() { + *x = GetProjectRequest{} + mi := &file_project_v2_project_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProjectRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProjectRequest) ProtoMessage() {} + +func (x *GetProjectRequest) ProtoReflect() protoreflect.Message { + mi := &file_project_v2_project_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetProjectRequest.ProtoReflect.Descriptor instead. +func (*GetProjectRequest) Descriptor() ([]byte, []int) { + return file_project_v2_project_proto_rawDescGZIP(), []int{5} +} + +func (x *GetProjectRequest) GetProjectId() string { + if x != nil { + return x.ProjectId + } + return "" +} + +type GetProjectResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Project *Project `protobuf:"bytes,1,opt,name=project,proto3" json:"project,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProjectResponse) Reset() { + *x = GetProjectResponse{} + mi := &file_project_v2_project_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProjectResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProjectResponse) ProtoMessage() {} + +func (x *GetProjectResponse) ProtoReflect() protoreflect.Message { + mi := &file_project_v2_project_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetProjectResponse.ProtoReflect.Descriptor instead. +func (*GetProjectResponse) Descriptor() ([]byte, []int) { + return file_project_v2_project_proto_rawDescGZIP(), []int{6} +} + +func (x *GetProjectResponse) GetProject() *Project { + if x != nil { + return x.Project + } + return nil +} + +var File_project_v2_project_proto protoreflect.FileDescriptor + +const file_project_v2_project_proto_rawDesc = "" + + "\n" + + "\x18project/v2/project.proto\x12\n" + + "project.v2\x1a\x1cgoogle/api/annotations.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xa3\x02\n" + + "\aProject\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x129\n" + + "\n" + + "created_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" + + "\n" + + "updated_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\x12\x12\n" + + "\x04name\x18\x04 \x01(\tR\x04name\x12\x1e\n" + + "\vroot_doc_id\x18\x05 \x01(\tR\trootDocId\x12:\n" + + "\vroot_folder\x18\x06 \x01(\v2\x19.project.v2.ProjectFolderR\n" + + "rootFolder\x12\"\n" + + "\finstructions\x18\a \x01(\tR\finstructions\"\x94\x01\n" + + "\rProjectFolder\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12*\n" + + "\x04docs\x18\x03 \x03(\v2\x16.project.v2.ProjectDocR\x04docs\x123\n" + + "\afolders\x18\x04 \x03(\v2\x19.project.v2.ProjectFolderR\afolders\"\x84\x01\n" + + "\n" + + "ProjectDoc\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x18\n" + + "\aversion\x18\x02 \x01(\x05R\aversion\x12\x1a\n" + + "\bfilename\x18\x03 \x01(\tR\bfilename\x12\x1a\n" + + "\bfilepath\x18\x04 \x01(\tR\bfilepath\x12\x14\n" + + "\x05lines\x18\x05 \x03(\tR\x05lines\"\x97\x02\n" + + "\x14UpsertProjectRequest\x12\x1d\n" + + "\n" + + "project_id\x18\x01 \x01(\tR\tprojectId\x12\x17\n" + + "\x04name\x18\x02 \x01(\tH\x00R\x04name\x88\x01\x01\x12#\n" + + "\vroot_doc_id\x18\x03 \x01(\tH\x01R\trootDocId\x88\x01\x01\x12?\n" + + "\vroot_folder\x18\x04 \x01(\v2\x19.project.v2.ProjectFolderH\x02R\n" + + "rootFolder\x88\x01\x01\x12'\n" + + "\finstructions\x18\x05 \x01(\tH\x03R\finstructions\x88\x01\x01B\a\n" + + "\x05_nameB\x0e\n" + + "\f_root_doc_idB\x0e\n" + + "\f_root_folderB\x0f\n" + + "\r_instructions\"F\n" + + "\x15UpsertProjectResponse\x12-\n" + + "\aproject\x18\x01 \x01(\v2\x13.project.v2.ProjectR\aproject\"2\n" + + "\x11GetProjectRequest\x12\x1d\n" + + "\n" + + "project_id\x18\x01 \x01(\tR\tprojectId\"C\n" + + "\x12GetProjectResponse\x12-\n" + + "\aproject\x18\x01 \x01(\v2\x13.project.v2.ProjectR\aproject2\x8d\x02\n" + + "\x0eProjectService\x12\x82\x01\n" + + "\rUpsertProject\x12 .project.v2.UpsertProjectRequest\x1a!.project.v2.UpsertProjectResponse\",\x82\xd3\xe4\x93\x02&:\x01*\x1a!/_pd/api/v2/projects/{project_id}\x12v\n" + + "\n" + + "GetProject\x12\x1d.project.v2.GetProjectRequest\x1a\x1e.project.v2.GetProjectResponse\")\x82\xd3\xe4\x93\x02#\x12!/_pd/api/v2/projects/{project_id}B\x97\x01\n" + + "\x0ecom.project.v2B\fProjectProtoP\x01Z.paperdebugger/pkg/gen/api/project/v2;projectv2\xa2\x02\x03PXX\xaa\x02\n" + + "Project.V2\xca\x02\n" + + "Project\\V2\xe2\x02\x16Project\\V2\\GPBMetadata\xea\x02\vProject::V2b\x06proto3" + +var ( + file_project_v2_project_proto_rawDescOnce sync.Once + file_project_v2_project_proto_rawDescData []byte +) + +func file_project_v2_project_proto_rawDescGZIP() []byte { + file_project_v2_project_proto_rawDescOnce.Do(func() { + file_project_v2_project_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_project_v2_project_proto_rawDesc), len(file_project_v2_project_proto_rawDesc))) + }) + return file_project_v2_project_proto_rawDescData +} + +var file_project_v2_project_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_project_v2_project_proto_goTypes = []any{ + (*Project)(nil), // 0: project.v2.Project + (*ProjectFolder)(nil), // 1: project.v2.ProjectFolder + (*ProjectDoc)(nil), // 2: project.v2.ProjectDoc + (*UpsertProjectRequest)(nil), // 3: project.v2.UpsertProjectRequest + (*UpsertProjectResponse)(nil), // 4: project.v2.UpsertProjectResponse + (*GetProjectRequest)(nil), // 5: project.v2.GetProjectRequest + (*GetProjectResponse)(nil), // 6: project.v2.GetProjectResponse + (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp +} +var file_project_v2_project_proto_depIdxs = []int32{ + 7, // 0: project.v2.Project.created_at:type_name -> google.protobuf.Timestamp + 7, // 1: project.v2.Project.updated_at:type_name -> google.protobuf.Timestamp + 1, // 2: project.v2.Project.root_folder:type_name -> project.v2.ProjectFolder + 2, // 3: project.v2.ProjectFolder.docs:type_name -> project.v2.ProjectDoc + 1, // 4: project.v2.ProjectFolder.folders:type_name -> project.v2.ProjectFolder + 1, // 5: project.v2.UpsertProjectRequest.root_folder:type_name -> project.v2.ProjectFolder + 0, // 6: project.v2.UpsertProjectResponse.project:type_name -> project.v2.Project + 0, // 7: project.v2.GetProjectResponse.project:type_name -> project.v2.Project + 3, // 8: project.v2.ProjectService.UpsertProject:input_type -> project.v2.UpsertProjectRequest + 5, // 9: project.v2.ProjectService.GetProject:input_type -> project.v2.GetProjectRequest + 4, // 10: project.v2.ProjectService.UpsertProject:output_type -> project.v2.UpsertProjectResponse + 6, // 11: project.v2.ProjectService.GetProject:output_type -> project.v2.GetProjectResponse + 10, // [10:12] is the sub-list for method output_type + 8, // [8:10] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name +} + +func init() { file_project_v2_project_proto_init() } +func file_project_v2_project_proto_init() { + if File_project_v2_project_proto != nil { + return + } + file_project_v2_project_proto_msgTypes[3].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_project_v2_project_proto_rawDesc), len(file_project_v2_project_proto_rawDesc)), + NumEnums: 0, + NumMessages: 7, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_project_v2_project_proto_goTypes, + DependencyIndexes: file_project_v2_project_proto_depIdxs, + MessageInfos: file_project_v2_project_proto_msgTypes, + }.Build() + File_project_v2_project_proto = out.File + file_project_v2_project_proto_goTypes = nil + file_project_v2_project_proto_depIdxs = nil +} diff --git a/pkg/gen/api/project/v2/project.pb.gw.go b/pkg/gen/api/project/v2/project.pb.gw.go new file mode 100644 index 00000000..bb300096 --- /dev/null +++ b/pkg/gen/api/project/v2/project.pb.gw.go @@ -0,0 +1,253 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: project/v2/project.proto + +/* +Package projectv2 is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package projectv2 + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +func request_ProjectService_UpsertProject_0(ctx context.Context, marshaler runtime.Marshaler, client ProjectServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpsertProjectRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["project_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "project_id") + } + protoReq.ProjectId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "project_id", err) + } + msg, err := client.UpsertProject(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ProjectService_UpsertProject_0(ctx context.Context, marshaler runtime.Marshaler, server ProjectServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpsertProjectRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["project_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "project_id") + } + protoReq.ProjectId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "project_id", err) + } + msg, err := server.UpsertProject(ctx, &protoReq) + return msg, metadata, err +} + +func request_ProjectService_GetProject_0(ctx context.Context, marshaler runtime.Marshaler, client ProjectServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetProjectRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["project_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "project_id") + } + protoReq.ProjectId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "project_id", err) + } + msg, err := client.GetProject(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ProjectService_GetProject_0(ctx context.Context, marshaler runtime.Marshaler, server ProjectServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetProjectRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["project_id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "project_id") + } + protoReq.ProjectId, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "project_id", err) + } + msg, err := server.GetProject(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterProjectServiceHandlerServer registers the http handlers for service ProjectService to "mux". +// UnaryRPC :call ProjectServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterProjectServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterProjectServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ProjectServiceServer) error { + mux.Handle(http.MethodPut, pattern_ProjectService_UpsertProject_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/project.v2.ProjectService/UpsertProject", runtime.WithHTTPPathPattern("/_pd/api/v2/projects/{project_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ProjectService_UpsertProject_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ProjectService_UpsertProject_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_ProjectService_GetProject_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/project.v2.ProjectService/GetProject", runtime.WithHTTPPathPattern("/_pd/api/v2/projects/{project_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ProjectService_GetProject_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ProjectService_GetProject_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterProjectServiceHandlerFromEndpoint is same as RegisterProjectServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterProjectServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterProjectServiceHandler(ctx, mux, conn) +} + +// RegisterProjectServiceHandler registers the http handlers for service ProjectService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterProjectServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterProjectServiceHandlerClient(ctx, mux, NewProjectServiceClient(conn)) +} + +// RegisterProjectServiceHandlerClient registers the http handlers for service ProjectService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "ProjectServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "ProjectServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "ProjectServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterProjectServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ProjectServiceClient) error { + mux.Handle(http.MethodPut, pattern_ProjectService_UpsertProject_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/project.v2.ProjectService/UpsertProject", runtime.WithHTTPPathPattern("/_pd/api/v2/projects/{project_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ProjectService_UpsertProject_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ProjectService_UpsertProject_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_ProjectService_GetProject_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/project.v2.ProjectService/GetProject", runtime.WithHTTPPathPattern("/_pd/api/v2/projects/{project_id}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ProjectService_GetProject_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ProjectService_GetProject_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_ProjectService_UpsertProject_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"_pd", "api", "v2", "projects", "project_id"}, "")) + pattern_ProjectService_GetProject_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"_pd", "api", "v2", "projects", "project_id"}, "")) +) + +var ( + forward_ProjectService_UpsertProject_0 = runtime.ForwardResponseMessage + forward_ProjectService_GetProject_0 = runtime.ForwardResponseMessage +) diff --git a/pkg/gen/api/project/v2/project_grpc.pb.go b/pkg/gen/api/project/v2/project_grpc.pb.go new file mode 100644 index 00000000..5ffe7ee4 --- /dev/null +++ b/pkg/gen/api/project/v2/project_grpc.pb.go @@ -0,0 +1,159 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.0 +// - protoc (unknown) +// source: project/v2/project.proto + +package projectv2 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ProjectService_UpsertProject_FullMethodName = "/project.v2.ProjectService/UpsertProject" + ProjectService_GetProject_FullMethodName = "/project.v2.ProjectService/GetProject" +) + +// ProjectServiceClient is the client API for ProjectService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ProjectServiceClient interface { + UpsertProject(ctx context.Context, in *UpsertProjectRequest, opts ...grpc.CallOption) (*UpsertProjectResponse, error) + GetProject(ctx context.Context, in *GetProjectRequest, opts ...grpc.CallOption) (*GetProjectResponse, error) +} + +type projectServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewProjectServiceClient(cc grpc.ClientConnInterface) ProjectServiceClient { + return &projectServiceClient{cc} +} + +func (c *projectServiceClient) UpsertProject(ctx context.Context, in *UpsertProjectRequest, opts ...grpc.CallOption) (*UpsertProjectResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpsertProjectResponse) + err := c.cc.Invoke(ctx, ProjectService_UpsertProject_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *projectServiceClient) GetProject(ctx context.Context, in *GetProjectRequest, opts ...grpc.CallOption) (*GetProjectResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetProjectResponse) + err := c.cc.Invoke(ctx, ProjectService_GetProject_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ProjectServiceServer is the server API for ProjectService service. +// All implementations must embed UnimplementedProjectServiceServer +// for forward compatibility. +type ProjectServiceServer interface { + UpsertProject(context.Context, *UpsertProjectRequest) (*UpsertProjectResponse, error) + GetProject(context.Context, *GetProjectRequest) (*GetProjectResponse, error) + mustEmbedUnimplementedProjectServiceServer() +} + +// UnimplementedProjectServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedProjectServiceServer struct{} + +func (UnimplementedProjectServiceServer) UpsertProject(context.Context, *UpsertProjectRequest) (*UpsertProjectResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpsertProject not implemented") +} +func (UnimplementedProjectServiceServer) GetProject(context.Context, *GetProjectRequest) (*GetProjectResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetProject not implemented") +} +func (UnimplementedProjectServiceServer) mustEmbedUnimplementedProjectServiceServer() {} +func (UnimplementedProjectServiceServer) testEmbeddedByValue() {} + +// UnsafeProjectServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ProjectServiceServer will +// result in compilation errors. +type UnsafeProjectServiceServer interface { + mustEmbedUnimplementedProjectServiceServer() +} + +func RegisterProjectServiceServer(s grpc.ServiceRegistrar, srv ProjectServiceServer) { + // If the following call panics, it indicates UnimplementedProjectServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ProjectService_ServiceDesc, srv) +} + +func _ProjectService_UpsertProject_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpsertProjectRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProjectServiceServer).UpsertProject(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProjectService_UpsertProject_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProjectServiceServer).UpsertProject(ctx, req.(*UpsertProjectRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ProjectService_GetProject_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetProjectRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProjectServiceServer).GetProject(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ProjectService_GetProject_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProjectServiceServer).GetProject(ctx, req.(*GetProjectRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ProjectService_ServiceDesc is the grpc.ServiceDesc for ProjectService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ProjectService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "project.v2.ProjectService", + HandlerType: (*ProjectServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "UpsertProject", + Handler: _ProjectService_UpsertProject_Handler, + }, + { + MethodName: "GetProject", + Handler: _ProjectService_GetProject_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "project/v2/project.proto", +} diff --git a/pkg/gen/api/shared/v1/shared.pb.go b/pkg/gen/api/shared/v1/shared.pb.go index 5c3eb7c8..0eb3183f 100644 --- a/pkg/gen/api/shared/v1/shared.pb.go +++ b/pkg/gen/api/shared/v1/shared.pb.go @@ -150,6 +150,77 @@ func (x *Error) GetMessage() string { return "" } +// OverleafAuth contains authentication information for Overleaf API calls. +// This is passed from frontend on each request and used by tools that need +// to interact with Overleaf directly (e.g., create_file, delete_file). +type OverleafAuth struct { + state protoimpl.MessageState `protogen:"open.v1"` + Session string `protobuf:"bytes,1,opt,name=session,proto3" json:"session,omitempty"` // overleaf_session2 cookie + Gclb string `protobuf:"bytes,2,opt,name=gclb,proto3" json:"gclb,omitempty"` // GCLB cookie (Google Cloud Load Balancer) + ProjectId string `protobuf:"bytes,3,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"` // Current Overleaf project ID + CsrfToken string `protobuf:"bytes,4,opt,name=csrf_token,json=csrfToken,proto3" json:"csrf_token,omitempty"` // csrf token for user's webpage + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OverleafAuth) Reset() { + *x = OverleafAuth{} + mi := &file_shared_v1_shared_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OverleafAuth) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OverleafAuth) ProtoMessage() {} + +func (x *OverleafAuth) ProtoReflect() protoreflect.Message { + mi := &file_shared_v1_shared_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OverleafAuth.ProtoReflect.Descriptor instead. +func (*OverleafAuth) Descriptor() ([]byte, []int) { + return file_shared_v1_shared_proto_rawDescGZIP(), []int{1} +} + +func (x *OverleafAuth) GetSession() string { + if x != nil { + return x.Session + } + return "" +} + +func (x *OverleafAuth) GetGclb() string { + if x != nil { + return x.Gclb + } + return "" +} + +func (x *OverleafAuth) GetProjectId() string { + if x != nil { + return x.ProjectId + } + return "" +} + +func (x *OverleafAuth) GetCsrfToken() string { + if x != nil { + return x.CsrfToken + } + return "" +} + var File_shared_v1_shared_proto protoreflect.FileDescriptor const file_shared_v1_shared_proto_rawDesc = "" + @@ -157,7 +228,14 @@ const file_shared_v1_shared_proto_rawDesc = "" + "\x16shared/v1/shared.proto\x12\tshared.v1\"K\n" + "\x05Error\x12(\n" + "\x04code\x18\x02 \x01(\x0e2\x14.shared.v1.ErrorCodeR\x04code\x12\x18\n" + - "\amessage\x18\x03 \x01(\tR\amessage*\x87\x03\n" + + "\amessage\x18\x03 \x01(\tR\amessage\"z\n" + + "\fOverleafAuth\x12\x18\n" + + "\asession\x18\x01 \x01(\tR\asession\x12\x12\n" + + "\x04gclb\x18\x02 \x01(\tR\x04gclb\x12\x1d\n" + + "\n" + + "project_id\x18\x03 \x01(\tR\tprojectId\x12\x1d\n" + + "\n" + + "csrf_token\x18\x04 \x01(\tR\tcsrfToken*\x87\x03\n" + "\tErrorCode\x12\x1a\n" + "\x16ERROR_CODE_UNSPECIFIED\x10\x00\x12\x17\n" + "\x12ERROR_CODE_UNKNOWN\x10\xe8\a\x12\x18\n" + @@ -187,10 +265,11 @@ func file_shared_v1_shared_proto_rawDescGZIP() []byte { } var file_shared_v1_shared_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_shared_v1_shared_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_shared_v1_shared_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_shared_v1_shared_proto_goTypes = []any{ - (ErrorCode)(0), // 0: shared.v1.ErrorCode - (*Error)(nil), // 1: shared.v1.Error + (ErrorCode)(0), // 0: shared.v1.ErrorCode + (*Error)(nil), // 1: shared.v1.Error + (*OverleafAuth)(nil), // 2: shared.v1.OverleafAuth } var file_shared_v1_shared_proto_depIdxs = []int32{ 0, // 0: shared.v1.Error.code:type_name -> shared.v1.ErrorCode @@ -212,7 +291,7 @@ func file_shared_v1_shared_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_shared_v1_shared_proto_rawDesc), len(file_shared_v1_shared_proto_rawDesc)), NumEnums: 1, - NumMessages: 1, + NumMessages: 2, NumExtensions: 0, NumServices: 0, }, diff --git a/proto/chat/v2/chat.proto b/proto/chat/v2/chat.proto index 68130275..eb7858e1 100644 --- a/proto/chat/v2/chat.proto +++ b/proto/chat/v2/chat.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package chat.v2; import "google/api/annotations.proto"; +import "shared/v1/shared.proto"; option go_package = "paperdebugger/pkg/gen/api/chat/v2;chatv2"; @@ -52,6 +53,7 @@ message MessageTypeSystem { message MessageTypeAssistant { string content = 1; string model_slug = 2; + optional string reasoning = 3; } message MessageTypeUser { @@ -81,12 +83,24 @@ message Message { int64 timestamp = 3; } +message BranchInfo { + string id = 1; + int64 created_at = 2; + int64 updated_at = 3; +} + message Conversation { string id = 1; string title = 2; string model_slug = 3; // If list conversations, then messages length is 0. repeated Message messages = 4; + + // Branch information + string current_branch_id = 5; // ID of the branch being viewed + repeated BranchInfo branches = 6; // All available branches + int32 current_branch_index = 7; // 1-indexed position for UI + int32 total_branches = 8; // Total number of branches } message ListConversationsRequest { @@ -100,6 +114,7 @@ message ListConversationsResponse { message GetConversationRequest { string conversation_id = 1; + optional string branch_id = 2; // Optional: specific branch to view } message GetConversationResponse { @@ -130,6 +145,8 @@ message SupportedModel { int64 max_output = 4; int64 input_price = 5; // in cents per 1M tokens int64 output_price = 6; // in cents per 1M tokens + bool disabled = 7; // Whether the model is disabled + optional string disabled_reason = 8; // Reason why the model is disabled } message ListSupportedModelsRequest { @@ -165,6 +182,11 @@ message MessageChunk { string delta = 2; // The small piece of text } +message ReasoningChunk { + string message_id = 1; // The id of the message that this chunk belongs to + string delta = 2; // The small piece of text +} + message IncompleteIndicator { string reason = 1; string response_id = 2; @@ -211,6 +233,9 @@ message CreateConversationMessageStreamRequest { optional string user_selected_text = 5; optional ConversationType conversation_type = 6; optional string surrounding = 8; + optional string parent_message_id = 9; + // Overleaf authentication for tools that need direct Overleaf API access + optional shared.v1.OverleafAuth overleaf_auth = 10; } // Response for streaming a message within an existing conversation @@ -223,5 +248,6 @@ message CreateConversationMessageStreamResponse { StreamPartEnd stream_part_end = 5; StreamFinalization stream_finalization = 6; StreamError stream_error = 7; + ReasoningChunk reasoning_chunk = 8; } } diff --git a/proto/project/v2/project.proto b/proto/project/v2/project.proto new file mode 100644 index 00000000..19fc489e --- /dev/null +++ b/proto/project/v2/project.proto @@ -0,0 +1,65 @@ +syntax = "proto3"; + +package project.v2; + +import "google/api/annotations.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "paperdebugger/pkg/gen/api/project/v2;projectv2"; + +service ProjectService { + rpc UpsertProject(UpsertProjectRequest) returns (UpsertProjectResponse) { + option (google.api.http) = { + put: "/_pd/api/v2/projects/{project_id}" + body: "*" + }; + } + rpc GetProject(GetProjectRequest) returns (GetProjectResponse) { + option (google.api.http) = {get: "/_pd/api/v2/projects/{project_id}"}; + } +} + +message Project { + string id = 1; + google.protobuf.Timestamp created_at = 2; + google.protobuf.Timestamp updated_at = 3; + string name = 4; + string root_doc_id = 5; + ProjectFolder root_folder = 6; + string instructions = 7; +} + +message ProjectFolder { + string id = 1; + string name = 2; + repeated ProjectDoc docs = 3; + repeated ProjectFolder folders = 4; +} + +message ProjectDoc { + string id = 1; + int32 version = 2; + string filename = 3; + string filepath = 4; + repeated string lines = 5; +} + +message UpsertProjectRequest { + string project_id = 1; + optional string name = 2; + optional string root_doc_id = 3; + optional ProjectFolder root_folder = 4; + optional string instructions = 5; +} + +message UpsertProjectResponse { + Project project = 1; +} + +message GetProjectRequest { + string project_id = 1; +} + +message GetProjectResponse { + Project project = 1; +} diff --git a/proto/shared/v1/shared.proto b/proto/shared/v1/shared.proto index 80983524..804871e3 100644 --- a/proto/shared/v1/shared.proto +++ b/proto/shared/v1/shared.proto @@ -24,3 +24,13 @@ message Error { ErrorCode code = 2; string message = 3; } + +// OverleafAuth contains authentication information for Overleaf API calls. +// This is passed from frontend on each request and used by tools that need +// to interact with Overleaf directly (e.g., create_file, delete_file). +message OverleafAuth { + string session = 1; // overleaf_session2 cookie + string gclb = 2; // GCLB cookie (Google Cloud Load Balancer) + string project_id = 3; // Current Overleaf project ID + string csrf_token = 4; // csrf token for user's webpage +} diff --git a/webapp/_webapp/bun.lock b/webapp/_webapp/bun.lock index c4d24488..f36b144f 100644 --- a/webapp/_webapp/bun.lock +++ b/webapp/_webapp/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "webapp", @@ -14,6 +13,10 @@ "@iconify/react": "^6.0.0", "@lukemorales/query-key-factory": "^1.3.4", "@r2wc/react-to-web-component": "^2.1.0", + "@streamdown/cjk": "^1.0.1", + "@streamdown/code": "^1.0.1", + "@streamdown/math": "^1.0.1", + "@streamdown/mermaid": "^1.0.1", "@tanstack/react-query": "^5.79.0", "@types/diff": "^8.0.0", "@uidotdev/usehooks": "^2.4.1", @@ -33,6 +36,7 @@ "react-dom": "^19.1.0", "react-rnd": "^10.5.2", "semver": "^7.7.2", + "streamdown": "^2.1.0", "uuid": "^11.1.0", "zustand": "^5.0.5", }, @@ -67,8 +71,12 @@ "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], + "@babel/runtime": ["@babel/runtime@7.27.4", "", {}, "sha512-t3yaEOuGu9NlIZ+hIeGbBjFtZT7j2cb2tg0fuaJKeGotchRjjLfrBA9Kwf8quhpP1EUuxModQg04q/mBwyg8uA=="], + "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.1", "", {}, "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw=="], + "@buf/googleapis_googleapis.bufbuild_es": ["@buf/googleapis_googleapis.bufbuild_es@2.2.3-20250411203938-61b203b9a916.1", "https://buf.build/gen/npm/v1/@buf/googleapis_googleapis.bufbuild_es/-/googleapis_googleapis.bufbuild_es-2.2.3-20250411203938-61b203b9a916.1.tgz", { "peerDependencies": { "@bufbuild/protobuf": "^2.2.3" } }, ""], "@bufbuild/protobuf": ["@bufbuild/protobuf@2.5.1", "", {}, "sha512-lut4UTvKL8tqtend0UDu7R79/n9jA7Jtxf77RNPbxtmWqfWI4qQ9bTjf7KCS4vfqLmpQbuHr1ciqJumAgJODdw=="], @@ -77,6 +85,16 @@ "@capacitor/core": ["@capacitor/core@7.4.0", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-P6NnjoHyobZgTjynlZSn27d0SUj6j38inlNxFnKZr9qwU7/r6+0Sg2nWkGkIH/pMmXHsvGD8zVe6KUq1UncIjA=="], + "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@11.0.3", "", { "dependencies": { "@chevrotain/gast": "11.0.3", "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ=="], + + "@chevrotain/gast": ["@chevrotain/gast@11.0.3", "", { "dependencies": { "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q=="], + + "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@11.0.3", "", {}, "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA=="], + + "@chevrotain/types": ["@chevrotain/types@11.0.3", "", {}, "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="], + + "@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="], + "@codemirror/state": ["@codemirror/state@6.5.2", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA=="], "@codemirror/view": ["@codemirror/view@6.37.1", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-Qy4CAUwngy/VQkEz0XzMKVRcckQuqLYWKqVpDDDghBe5FSXSqfVrJn49nw3ePZHxRUz4nRmb05Lgi+9csWo4eg=="], @@ -333,6 +351,8 @@ "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + "@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="], + "@internationalized/date": ["@internationalized/date@3.8.1", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-PgVE6B6eIZtzf9Gu5HvJxRK3ufUFz9DhspELuhW/N0GuMGMTLvPQNRkHP2hTuP9lblOk+f+1xi96sPiPXANXAA=="], "@internationalized/message": ["@internationalized/message@3.1.7", "", { "dependencies": { "@swc/helpers": "^0.5.0", "intl-messageformat": "^10.1.0" } }, "sha512-gLQlhEW4iO7DEFPf/U7IrIdA3UyLGS0opeqouaFwlMObLUzwexRjbygONHDVbC9G9oFLXsLyGKYkJwqXw/QADg=="], @@ -359,6 +379,8 @@ "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], + "@mermaid-js/parser": ["@mermaid-js/parser@0.6.3", "", { "dependencies": { "langium": "3.3.1" } }, "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -633,6 +655,28 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.41.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw=="], + "@shikijs/core": ["@shikijs/core@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ=="], + + "@shikijs/langs": ["@shikijs/langs@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0" } }, "sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA=="], + + "@shikijs/themes": ["@shikijs/themes@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0" } }, "sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw=="], + + "@shikijs/types": ["@shikijs/types@3.21.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + + "@streamdown/cjk": ["@streamdown/cjk@1.0.1", "", { "dependencies": { "remark-cjk-friendly": "^1.2.3", "remark-cjk-friendly-gfm-strikethrough": "^1.2.3", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-ElDoEfad2u8iFzmgmEEab15N4mt19r47xeUIPJtHaHVyEF5baojamGo+xw3MywMj2qUsAY3LnTnKbrUtL5tGkg=="], + + "@streamdown/code": ["@streamdown/code@1.0.1", "", { "dependencies": { "shiki": "^3.19.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-U9LITfQ28tZYAoY922jdtw1ryg4kgRBdURopqK9hph7G2fBUwPeHthjH7SvaV0fvFv7EqjqCzARJuWUljLe9Ag=="], + + "@streamdown/math": ["@streamdown/math@1.0.1", "", { "dependencies": { "katex": "^0.16.27", "rehype-katex": "^7.0.1", "remark-math": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-R9WdHbpERiRU7WeO7oT1aIbnLJ/jraDr89F7X9x2OM//Y8G8UMATRnLD/RUwg4VLr8Nu7QSIJ0Pa8lXd2meM4Q=="], + + "@streamdown/mermaid": ["@streamdown/mermaid@1.0.1", "", { "dependencies": { "mermaid": "^11.12.2" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-LVGbxYd6t1DKMCMqm3cpbfsdD4/EKpQelanOlJaBMKv83kbrl8syZJhVBsd/jka+CawhpeR9xsGQJzSJEpjoVw=="], + "@swc/core": ["@swc/core@1.11.29", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.21" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.11.29", "@swc/core-darwin-x64": "1.11.29", "@swc/core-linux-arm-gnueabihf": "1.11.29", "@swc/core-linux-arm64-gnu": "1.11.29", "@swc/core-linux-arm64-musl": "1.11.29", "@swc/core-linux-x64-gnu": "1.11.29", "@swc/core-linux-x64-musl": "1.11.29", "@swc/core-win32-arm64-msvc": "1.11.29", "@swc/core-win32-ia32-msvc": "1.11.29", "@swc/core-win32-x64-msvc": "1.11.29" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" } }, "sha512-g4mThMIpWbNhV8G2rWp5a5/Igv8/2UFRJx2yImrLGMgrDDYZIopqZ/z0jZxDgqNA1QDx93rpwNF7jGsxVWcMlA=="], "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.11.29", "", { "os": "darwin", "cpu": "arm64" }, "sha512-whsCX7URzbuS5aET58c75Dloby3Gtj/ITk2vc4WW6pSDQKSPDuONsIcZ7B2ng8oz0K6ttbi4p3H/PNPQLJ4maQ=="], @@ -675,24 +719,100 @@ "@types/codemirror": ["@types/codemirror@5.60.16", "", { "dependencies": { "@types/tern": "*" } }, "sha512-V/yHdamffSS075jit+fDxaOAmdP2liok8NSNJnAZfDJErzOheuygHZEhAJrfmk5TEyM32MhkZjwo/idX791yxw=="], + "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], + + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], + + "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], + + "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], + + "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], + + "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], + + "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], + + "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], + + "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], + + "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], + + "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="], + + "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], + + "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], + + "@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], + + "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + + "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], + + "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], + + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + "@types/diff": ["@types/diff@8.0.0", "", { "dependencies": { "diff": "*" } }, "sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw=="], "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + "@types/events": ["@types/events@3.0.3", "", {}, "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g=="], "@types/filesystem": ["@types/filesystem@0.0.36", "", { "dependencies": { "@types/filewriter": "*" } }, "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA=="], "@types/filewriter": ["@types/filewriter@0.0.33", "", {}, "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g=="], + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + "@types/har-format": ["@types/har-format@1.2.16", "", {}, "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A=="], + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="], + "@types/lodash": ["@types/lodash@4.17.17", "", {}, "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ=="], "@types/lodash.debounce": ["@types/lodash.debounce@4.0.9", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ=="], + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/node": ["@types/node@22.15.29", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ=="], "@types/react": ["@types/react@19.1.6", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q=="], @@ -705,6 +825,10 @@ "@types/tern": ["@types/tern@0.23.9", "", { "dependencies": { "@types/estree": "*" } }, "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.33.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.33.0", "@typescript-eslint/type-utils": "8.33.0", "@typescript-eslint/utils": "8.33.0", "@typescript-eslint/visitor-keys": "8.33.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.33.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.33.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.33.0", "@typescript-eslint/types": "8.33.0", "@typescript-eslint/typescript-estree": "8.33.0", "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ=="], @@ -727,6 +851,8 @@ "@uidotdev/usehooks": ["@uidotdev/usehooks@2.4.1", "", { "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@3.10.0", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.9", "@swc/core": "^1.11.22" }, "peerDependencies": { "vite": "^4 || ^5 || ^6" } }, "sha512-ZmkdHw3wo/o/Rk05YsXZs/DJAfY2CdQ5DUAjoWji+PEr+hYADdGMCGgEAILbiKj+CjspBTuTACBcWDrmC8AUfw=="], "acorn": ["acorn@8.14.1", "", { "bin": "bin/acorn" }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], @@ -757,6 +883,8 @@ "axios": ["axios@1.9.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg=="], + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.9.12", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Mij6Lij93pTAIsSYy5cyBQ975Qh9uLEc5rwGTpomiZeXZL9yIS6uORJakb3ScHgfs0serMMfIbXzokPMuEiRyw=="], @@ -779,15 +907,29 @@ "caniuse-lite": ["caniuse-lite@1.0.30001762", "", {}, "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + + "chevrotain": ["chevrotain@11.0.3", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", "@chevrotain/regexp-to-ast": "11.0.3", "@chevrotain/types": "11.0.3", "@chevrotain/utils": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw=="], + + "chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="], + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], - "clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], @@ -801,14 +943,20 @@ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], "compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="], "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], + "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], "cross-fetch": ["cross-fetch@4.1.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw=="], @@ -819,22 +967,106 @@ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "cytoscape": ["cytoscape@3.33.1", "", {}, "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ=="], + + "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="], + + "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="], + + "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], + + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], + + "d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="], + + "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="], + + "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], + + "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], + + "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], + + "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="], + + "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], + + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + + "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], + + "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="], + + "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], + + "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], + + "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], + + "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], + + "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], + + "dagre-d3-es": ["dagre-d3-es@7.0.13", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q=="], + + "dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="], + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], "decimal.js": ["decimal.js@10.5.0", "", {}, "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="], + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + "delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + "dompurify": ["dompurify@3.3.1", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], @@ -843,6 +1075,8 @@ "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], @@ -875,10 +1109,14 @@ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], @@ -917,6 +1155,8 @@ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], @@ -931,6 +1171,8 @@ "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], + "has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], @@ -939,8 +1181,42 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="], + + "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], + + "hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw=="], + + "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], + + "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], + + "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], + + "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], + + "hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="], + + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + + "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], + + "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], + "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], + + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "ignore-by-default": ["ignore-by-default@1.0.1", "", {}, "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="], @@ -957,24 +1233,38 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + "input-otp": ["input-otp@1.4.1", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw=="], + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + "intl-messageformat": ["intl-messageformat@10.7.16", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.4", "@formatjs/fast-memoize": "2.2.7", "@formatjs/icu-messageformat-parser": "2.11.2", "tslib": "^2.8.0" } }, "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug=="], + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -995,8 +1285,16 @@ "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], + "katex": ["katex@0.16.27", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], + + "langium": ["langium@3.3.1", "", { "dependencies": { "chevrotain": "~11.0.3", "chevrotain-allstar": "~0.3.0", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.0.8" } }, "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w=="], + + "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], @@ -1007,22 +1305,128 @@ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lodash-es": ["lodash-es@4.17.22", "", {}, "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q=="], + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": "cli.js" }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "markdown-to-jsx": ["markdown-to-jsx@7.7.6", "", { "peerDependencies": { "react": ">= 0.14.0" } }, "sha512-/PWFFoKKMidk4Ut06F5hs5sluq1aJ0CGvUJWsnCK6hx/LPM8vlhvKAxtGHJ+U+V2Il2wmnfO6r81ICD3xZRVaw=="], + "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], + + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + + "mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="], + + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], + + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "mermaid": ["mermaid@11.12.2", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", "@mermaid-js/parser": "^0.6.3", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.13", "dayjs": "^1.11.18", "dompurify": "^3.2.5", "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.21", "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w=="], + + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-cjk-friendly": ["micromark-extension-cjk-friendly@1.2.3", "", { "dependencies": { "devlop": "^1.1.0", "micromark-extension-cjk-friendly-util": "2.1.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-gRzVLUdjXBLX6zNPSnHGDoo+ZTp5zy+MZm0g3sv+3chPXY7l9gW+DnrcHcZh/jiPR6MjPKO4AEJNp4Aw6V9z5Q=="], + + "micromark-extension-cjk-friendly-gfm-strikethrough": ["micromark-extension-cjk-friendly-gfm-strikethrough@1.2.3", "", { "dependencies": { "devlop": "^1.1.0", "get-east-asian-width": "^1.3.0", "micromark-extension-cjk-friendly-util": "2.1.1", "micromark-util-character": "^2.1.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-gSPnxgHDDqXYOBvQRq6lerrq9mjDhdtKn+7XETuXjxWcL62yZEfUdA28Ml1I2vDIPfAOIKLa0h2XDSGkInGHFQ=="], + + "micromark-extension-cjk-friendly-util": ["micromark-extension-cjk-friendly-util@2.1.1", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "micromark-util-character": "^2.1.1", "micromark-util-symbol": "^2.0.1" } }, "sha512-egs6+12JU2yutskHY55FyR48ZiEcFOJFyk9rsiyIhcJ6IvWB6ABBqVrBw8IobqJTDZ/wdSr9eoXDPb5S2nW1bg=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + + "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -1035,6 +1439,8 @@ "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], + "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], "motion-dom": ["motion-dom@12.15.0", "", { "dependencies": { "motion-utils": "^12.12.1" } }, "sha512-D2ldJgor+2vdcrDtKJw48k3OddXiZN1dDLLWrS8kiHzQdYVruh0IoTwbJBslrnTXIPgFED7PBN2Zbwl7rNqnhA=="], @@ -1063,6 +1469,10 @@ "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], + + "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], + "openai": ["openai@5.0.1", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": "bin/cli" }, "sha512-Do6vxhbDv7cXhji/4ct1lrpZYMAOmjYbhyA9LJTuG7OfpbWMpuS+EIXkRT7R+XxpRB1OZhU/op4FU3p3uxU6gw=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], @@ -1073,10 +1483,18 @@ "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -1085,6 +1503,8 @@ "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], @@ -1093,6 +1513,12 @@ "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], + + "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], + "postcss": ["postcss@8.5.4", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w=="], "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], @@ -1115,6 +1541,8 @@ "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], @@ -1145,6 +1573,36 @@ "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + + "rehype-harden": ["rehype-harden@1.1.7", "", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-j5DY0YSK2YavvNGV+qBHma15J9m0WZmRe8posT5AtKDS6TNWtMVTo6RiqF8SidfcASYz8f3k2J/1RWmq5zTXUw=="], + + "rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="], + + "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], + + "rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="], + + "remark-cjk-friendly": ["remark-cjk-friendly@1.2.3", "", { "dependencies": { "micromark-extension-cjk-friendly": "1.2.3" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-UvAgxwlNk+l9Oqgl/9MWK2eWRS7zgBW/nXX9AthV7nd/3lNejF138E7Xbmk9Zs4WjTJGs721r7fAEc7tNFoH7g=="], + + "remark-cjk-friendly-gfm-strikethrough": ["remark-cjk-friendly-gfm-strikethrough@1.2.3", "", { "dependencies": { "micromark-extension-cjk-friendly-gfm-strikethrough": "1.2.3" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-bXfMZtsaomK6ysNN/UGRIcasQAYkC10NtPmP0oOHOV8YOhA2TXmwRXCku4qOzjIFxAPfish5+XS0eIug2PzNZA=="], + + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + + "remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="], + + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + + "remend": ["remend@1.1.0", "", {}, "sha512-JENGyuIhTwzUfCarW43X4r9cehoqTo9QyYxfNDZSud2AmqeuWjZ5pfybasTa4q0dxTJAj5m8NB+wR+YueAFpxQ=="], + "require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], @@ -1153,12 +1611,20 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="], + "rollup": ["rollup@4.41.1", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.41.1", "@rollup/rollup-android-arm64": "4.41.1", "@rollup/rollup-darwin-arm64": "4.41.1", "@rollup/rollup-darwin-x64": "4.41.1", "@rollup/rollup-freebsd-arm64": "4.41.1", "@rollup/rollup-freebsd-x64": "4.41.1", "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", "@rollup/rollup-linux-arm-musleabihf": "4.41.1", "@rollup/rollup-linux-arm64-gnu": "4.41.1", "@rollup/rollup-linux-arm64-musl": "4.41.1", "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-musl": "4.41.1", "@rollup/rollup-linux-s390x-gnu": "4.41.1", "@rollup/rollup-linux-x64-gnu": "4.41.1", "@rollup/rollup-linux-x64-musl": "4.41.1", "@rollup/rollup-win32-arm64-msvc": "4.41.1", "@rollup/rollup-win32-ia32-msvc": "4.41.1", "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw=="], + "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], + "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], "scriptjs": ["scriptjs@2.5.9", "", {}, "sha512-qGVDoreyYiP1pkQnbnFAUIS5AjenNwwQBdl7zeos9etl+hYKWahjRTfzAZZYBv5xNHx7vNKCmaLDQZ6Fr2AEXg=="], @@ -1173,6 +1639,8 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "shiki": ["shiki@3.21.0", "", { "dependencies": { "@shikijs/core": "3.21.0", "@shikijs/engine-javascript": "3.21.0", "@shikijs/engine-oniguruma": "3.21.0", "@shikijs/langs": "3.21.0", "@shikijs/themes": "3.21.0", "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], @@ -1181,12 +1649,18 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "streamdown": ["streamdown@2.1.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "rehype-harden": "^1.1.7", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.1.0", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-u9gWd0AmjKg1d+74P44XaPlGrMeC21oDOSIhjGNEYMAttDMzCzlJO6lpTyJ9JkSinQQF65YcK4eOd3q9iTvULw=="], + "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1195,13 +1669,19 @@ "style-mod": ["style-mod@4.1.2", "", {}, "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="], + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], + + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + + "stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="], + "sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="], "supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "tailwind-merge": ["tailwind-merge@2.5.4", "", {}, "sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q=="], + "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], "tailwind-variants": ["tailwind-variants@0.3.0", "", { "dependencies": { "tailwind-merge": "^2.5.4" }, "peerDependencies": { "tailwindcss": "*" } }, "sha512-ho2k5kn+LB1fT5XdNS3Clb96zieWxbStE9wNLK7D0AV64kdZMaYzAKo0fWl6fXLPY99ffF9oBJnIj5escEl/8A=="], @@ -1213,6 +1693,8 @@ "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -1221,8 +1703,14 @@ "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="], + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -1235,10 +1723,28 @@ "ua-parser-js": ["ua-parser-js@1.0.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug=="], + "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], + "undefsafe": ["undefsafe@2.0.5", "", {}, "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + + "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], @@ -1255,10 +1761,30 @@ "uuid": ["uuid@11.1.0", "", { "bin": "dist/esm/bin/uuid" }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + "vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx"], "bin": "bin/vite.js" }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], + "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], + + "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="], + + "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="], + + "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="], + + "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], + + "vscode-uri": ["vscode-uri@3.0.8", "", {}, "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw=="], + "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], + "web-vitals": ["web-vitals@5.1.0", "", {}, "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg=="], "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], @@ -1281,22 +1807,30 @@ "zustand": ["zustand@5.0.5", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" } }, "sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg=="], + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + + "@chevrotain/cst-dts-gen/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], + + "@chevrotain/gast/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], "@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.15.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg=="], + "@heroui/system-rsc/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="], + + "@heroui/theme/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="], + + "@heroui/theme/tailwind-merge": ["tailwind-merge@2.5.4", "", {}, "sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q=="], + "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], "@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], "@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], - "@react-aria/focus/clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - - "@react-aria/utils/clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -1309,14 +1843,36 @@ "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "chevrotain/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], + + "d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + + "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], + + "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "mlly/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "react-draggable/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="], + "react-rnd/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="], "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -1327,6 +1883,8 @@ "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "tailwind-variants/tailwind-merge": ["tailwind-merge@2.5.4", "", {}, "sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q=="], + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1337,6 +1895,12 @@ "chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], + + "d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], + + "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], + "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], diff --git a/webapp/_webapp/docs/STREAMING_ARCHITECTURE.md b/webapp/_webapp/docs/STREAMING_ARCHITECTURE.md new file mode 100644 index 00000000..2ba84d7c --- /dev/null +++ b/webapp/_webapp/docs/STREAMING_ARCHITECTURE.md @@ -0,0 +1,389 @@ +# Streaming Architecture Documentation + +This document describes the refactored streaming architecture for the PaperDebugger chat system. + +## Overview + +The streaming system handles real-time message delivery from the server to the client, managing state transitions, error recovery, and UI updates. The architecture has been redesigned to be more maintainable, testable, and extensible. + +## Architecture Diagram + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ useSendMessageStream Hook │ +│ (Orchestrator - Single Responsibility) │ +│ - Builds stream requests using buildStreamRequest() │ +│ - Maps responses using mapResponseToStreamEvent() │ +│ - Delegates event handling to state machine │ +└─────────────────────────────────┬──────────────────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────────────────────────────────────────┐ +│ StreamingStateMachine │ +│ (Zustand Store + Event Handler) │ +│ State: idle | receiving | finalizing | error │ +│ Actions: handleEvent(), reset() │ +│ Data: streamingMessage, incompleteIndicator │ +└─────────────────────────────────┬──────────────────────────────────────────┘ + │ + ┌────────────────────────┼────────────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────────┐ +│ MessageType │ │ Error │ │ Conversation │ +│ Handlers │ │ Handler │ │ Store │ +│ (Registry) │ │ (Recovery) │ │ (Persistence) │ +└─────────────────┘ └──────────────────┘ └─────────────────────┘ + │ │ │ + │ │ │ + └────────────────────────┼────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────────────────────────────────────────┐ +│ MessageStore │ +│ (Unified Display Messages) │ +│ - Subscribes to ConversationStore (finalized messages) │ +│ - Subscribes to StreamingStateMachine (streaming messages) │ +│ - Provides getAllDisplayMessages() for UI components │ +└─────────────────────────────────┬──────────────────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────────────────────────────────────────┐ +│ UI Components │ +│ (ChatBody, MessageCard, etc.) │ +│ - Consume DisplayMessage type directly │ +│ - No knowledge of streaming internals │ +└────────────────────────────────────────────────────────────────────────────┘ +``` + +## Core Components + +### 1. StreamingStateMachine (`stores/streaming/streaming-state-machine.ts`) + +The central hub for all streaming state management. Implements a state machine pattern with the following states: + +| State | Description | Valid Transitions | +|-------|-------------|-------------------| +| `idle` | No active stream | → `receiving` (on INIT) | +| `receiving` | Actively receiving stream data | → `finalizing`, `error` | +| `finalizing` | Flushing data to conversation store | → `idle` | +| `error` | Error occurred during streaming | → `idle` (on reset) | + +#### Event Types + +```typescript +type StreamEvent = + | { type: "INIT"; payload: StreamInitialization } + | { type: "PART_BEGIN"; payload: StreamPartBegin } + | { type: "CHUNK"; payload: MessageChunk } + | { type: "REASONING_CHUNK"; payload: ReasoningChunk } + | { type: "PART_END"; payload: StreamPartEnd } + | { type: "FINALIZE"; payload: StreamFinalization } + | { type: "ERROR"; payload: StreamError } + | { type: "INCOMPLETE"; payload: IncompleteIndicator } + | { type: "CONNECTION_ERROR"; payload: Error }; +``` + +### 2. MessageTypeHandlers (`stores/streaming/message-type-handlers.ts`) + +A registry of handlers for different message types. Adding a new message type only requires: +1. Creating a new handler class implementing `MessageTypeHandler` +2. Registering it in the `messageTypeHandlers` registry + +Available handlers: +- `AssistantHandler` - Handles assistant messages +- `ToolCallPrepareHandler` - Handles tool call argument streaming +- `ToolCallHandler` - Handles completed tool calls +- `NoOpHandler` - For types that don't require streaming handling + +### 3. ErrorHandler (`stores/streaming/error-handler.ts`) + +Centralized error handling with configurable recovery strategies: + +| Error Code | Strategy | Behavior | +|------------|----------|----------| +| `PROJECT_OUT_OF_DATE` | sync-and-retry | Sync project, then retry | +| `NETWORK_ERROR` | retry | Exponential backoff, 3 attempts | +| `TIMEOUT` | retry | Linear backoff, 2 attempts | +| `RATE_LIMITED` | retry | Exponential backoff, 3 attempts | +| `SERVER_ERROR` | retry | Exponential backoff, 2 attempts | +| `INVALID_RESPONSE` | show-error | Display error toast | +| `AUTHENTICATION_ERROR` | show-error | Display error, require re-auth | +| `UNKNOWN` | show-error | Display generic error | + +### 4. MessageStore (`stores/message-store.ts`) + +Unified store that combines finalized and streaming messages: + +```typescript +interface MessageStore { + // State + messages: Message[]; // Finalized from API + streamingEntries: InternalMessage[]; // Currently streaming + + // Computed + getAllDisplayMessages(): DisplayMessage[]; + getVisibleDisplayMessages(): DisplayMessage[]; + + // Helpers + hasStreamingMessages(): boolean; + isWaitingForResponse(): boolean; +} +``` + +## Data Types + +### InternalMessage + +The canonical internal message format used throughout the application: + +```typescript +type InternalMessage = + | UserMessage + | AssistantMessage + | ToolCallMessage + | ToolCallPrepareMessage + | SystemMessage + | UnknownMessage; + +interface UserMessage { + id: string; + role: "user"; + status: MessageStatus; + data: { + content: string; + selectedText?: string; + surrounding?: string; + }; +} + +// Similar structures for other message types... +``` + +### DisplayMessage + +UI-friendly message format: + +```typescript +interface DisplayMessage { + id: string; + type: "user" | "assistant" | "toolCall" | "toolCallPrepare" | "error"; + status: "streaming" | "complete" | "error" | "stale"; + content: string; + // Role-specific optional fields + selectedText?: string; + reasoning?: string; + toolName?: string; + toolArgs?: string; + toolResult?: string; + toolError?: string; +} +``` + +## Data Flow + +### Happy Path: User Message → Response + +``` +1. User submits message + └── useSendMessageStream.sendMessageStream(message, selectedText) + └── buildStreamRequest() → API request + +2. User message added to streaming state + └── StreamingStateMachine.setState({ streamingMessage: { parts: [userMessage] } }) + +3. API stream begins + └── Server sends: streamInitialization + └── INIT event → Finalize user message, flush to conversation + +4. Assistant response streams + └── Server sends: streamPartBegin (assistant) + └── PART_BEGIN event → Create streaming assistant message + └── Server sends: messageChunk (delta: "Hello") + └── CHUNK event → Append to assistant content + └── Server sends: messageChunk (delta: " World") + └── CHUNK event → Append to assistant content + └── Server sends: streamPartEnd (assistant) + └── PART_END event → Mark as complete + +5. Stream completes + └── Server sends: streamFinalization + └── FINALIZE event → Flush to conversation, reset streaming state +``` + +### Error Recovery Flow + +``` +1. Error occurs during streaming + └── Server sends: streamError or connection fails + └── ERROR/CONNECTION_ERROR event + +2. ErrorHandler evaluates strategy + └── createStreamingError() → Categorize error + └── getRecoveryStrategy() → Determine recovery approach + +3. Execute recovery + └── retry: Attempt operation again with backoff + └── sync-and-retry: Sync project first, then retry + └── show-error: Display toast to user + └── abort: Stop processing +``` + +## File Structure + +``` +src/ +├── stores/ +│ ├── streaming/ +│ │ ├── index.ts # Module exports +│ │ ├── types.ts # Type definitions +│ │ ├── streaming-state-machine.ts # Main state machine +│ │ ├── message-type-handlers.ts # Handler registry +│ │ ├── error-handler.ts # Error handling +│ │ └── __tests__/ # Unit tests +│ │ ├── streaming-state-machine.test.ts +│ │ ├── message-type-handlers.test.ts +│ │ └── error-handler.test.ts +│ ├── message-store.ts # Unified message store +│ ├── converters.ts # Store-level converters +│ └── types.ts # Store types (DisplayMessage) +├── types/ +│ └── message.ts # InternalMessage definitions +├── utils/ +│ ├── message-converters.ts # API ↔ Internal converters +│ ├── stream-request-builder.ts # Request building +│ ├── stream-event-mapper.ts # Response → Event mapping +│ └── __tests__/ +│ └── message-converters.test.ts +├── hooks/ +│ └── useSendMessageStream.ts # Main orchestration hook +└── __tests__/ + └── streaming-flow.integration.test.ts +``` + +## Extension Points + +### Adding a New Message Type + +1. Define the type in `types/message.ts`: +```typescript +export interface NewMessageData { + // type-specific data +} + +export interface NewMessage extends InternalMessageBase { + role: "newType"; + data: NewMessageData; +} + +// Update InternalMessage union +export type InternalMessage = ... | NewMessage; +``` + +2. Create handler in `message-type-handlers.ts`: +```typescript +class NewTypeHandler implements MessageTypeHandler { + onPartBegin(partBegin: StreamPartBegin): InternalMessage | null { + // Create streaming message + } + + onPartEnd(partEnd: StreamPartEnd, existing: InternalMessage): InternalMessage | null { + // Finalize message + } +} + +// Register in messageTypeHandlers +``` + +3. Add converters in `utils/message-converters.ts`: +```typescript +// In fromApiMessage() +case "newType": + return { /* conversion */ }; + +// In toApiMessage() +case "newType": + return fromJson(MessageSchema, { /* conversion */ }); +``` + +### Adding a New Error Type + +1. Add error code in `stores/streaming/types.ts`: +```typescript +export type StreamingErrorCode = + | ... + | "NEW_ERROR_TYPE"; +``` + +2. Add detection in `error-handler.ts`: +```typescript +function detectErrorCodeFromMessage(message: string): StreamingErrorCode { + if (message.includes("new error pattern")) { + return "NEW_ERROR_TYPE"; + } + // ... +} +``` + +3. Configure recovery strategy: +```typescript +const DEFAULT_STRATEGIES: Record = { + NEW_ERROR_TYPE: { + type: "retry", + maxRetries: 2, + backoff: "exponential", + delayMs: 1000, + }, + // ... +}; +``` + +## Testing + +### Running Tests + +```bash +# Run all tests +bun test + +# Run specific test file +bun test src/stores/streaming/__tests__/streaming-state-machine.test.ts + +# Run with watch mode +bun test --watch +``` + +### Test Coverage + +- **Unit Tests**: State machine, handlers, error handler, converters +- **Integration Tests**: Complete streaming flows, error scenarios + +## Performance Considerations + +1. **Sequence Numbers**: Each update increments a sequence number, allowing React to detect changes efficiently. + +2. **Computed Selectors**: `getAllDisplayMessages()` is computed and cached, only recomputing when source data changes. + +3. **Subscription-based Updates**: MessageStore subscribes to source stores, avoiding polling. + +4. **No flushSync**: The architecture uses natural React batching, eliminating the need for `flushSync`. + +## Troubleshooting + +### Messages not appearing + +1. Check that the state machine is receiving events (add logging to `handleEvent`) +2. Verify MessageStore subscriptions are initialized +3. Check DisplayMessage conversion in `toDisplayMessage()` + +### Stale messages after error + +1. Verify error event is being dispatched +2. Check that the error handler is marking messages as stale +3. Ensure UI is properly rendering stale status + +### Retry not working + +1. Check error code detection in `createStreamingError()` +2. Verify recovery strategy is configured for the error type +3. Ensure sync/retry callbacks are provided to error handler diff --git a/webapp/_webapp/docs/STREAMING_DESIGN_ANALYSIS.md b/webapp/_webapp/docs/STREAMING_DESIGN_ANALYSIS.md new file mode 100644 index 00000000..ce575896 --- /dev/null +++ b/webapp/_webapp/docs/STREAMING_DESIGN_ANALYSIS.md @@ -0,0 +1,308 @@ +# Front-End Streaming Logic Complexity Analysis + +## Executive Summary + +The current streaming implementation is spread across **15+ files** with fragmented logic, inconsistent patterns, and multiple data transformations that make the codebase difficult to understand, maintain, and debug. + +--- + +## Architecture Overview + +### Current Data Flow + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ useSendMessageStream │ +│ (Main orchestrator hook - handles all stream event routing) │ +└─────────────────────────────────┬──────────────────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────────────────────────────────────────┐ +│ createConversationMessageStream │ +│ (API call returns ReadableStream) │ +└─────────────────────────────────┬──────────────────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────────────────────────────────────────┐ +│ processStream │ +│ (Parses NDJSON, calls onMessage callback for each chunk) │ +└─────────────────────────────────┬──────────────────────────────────────────┘ + │ + ┌─────────────────────────────┼─────────────────────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ +│ streamInit │ │ streamPartBegin │ │ streamFinalization │ +│ handler │ │ handler │ │ handler │ +└────────┬────────┘ └────────┬────────┘ └──────────┬──────────┘ + │ │ │ + │ ▼ │ + │ ┌─────────────────┐ │ + │ │ messageChunk │ │ + │ │ handler │ │ + │ └────────┬────────┘ │ + │ │ │ + │ ▼ │ + │ ┌─────────────────┐ │ + │ │ streamPartEnd │ │ + │ │ handler │ │ + │ └────────┬────────┘ │ + │ │ │ + ▼ ▼ ▼ +┌────────────────────────────────────────────────────────────────────────────┐ +│ streaming-message-store │ +│ { parts: MessageEntry[], sequence: number } │ +└─────────────────────────────────┬──────────────────────────────────────────┘ + │ + ▼ (flushStreamingMessageToConversation) +┌────────────────────────────────────────────────────────────────────────────┐ +│ conversation-store │ +│ { currentConversation: Conversation, isStreaming: boolean } │ +└─────────────────────────────────┬──────────────────────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────────────────────────────────────────┐ +│ ChatBody Component │ +│ (Renders both finalized and streaming messages) │ +└────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Complexity Problems + +### 1. **Fragmented Handler Architecture** (9 separate files) + +The streaming logic is scattered across 9 handler files in `/stores/conversation/handlers/`: + +| File | Purpose | Lines | +|------|---------|-------| +| `handleStreamInitialization.ts` | Mark user message as finalized | 29 | +| `handleStreamPartBegin.ts` | Create new MessageEntry for incoming part | 73 | +| `handleMessageChunk.ts` | Append delta text to assistant content | 37 | +| `handleStreamPartEnd.ts` | Mark part as finalized with full content | 99 | +| `handleStreamFinalization.ts` | Flush streaming messages to conversation | 6 | +| `handleStreamError.ts` | Handle stream errors with retry logic | 53 | +| `handleIncompleteIndicator.ts` | Set incomplete indicator state | 6 | +| `handleError.ts` | Mark all preparing messages as stale | 20 | +| `converter.ts` | Convert MessageEntry to Message | 69 | + +**Problem:** Following a single streaming flow requires jumping between 9+ files. Related logic is separated rather than cohesive. + +--- + +### 2. **Inconsistent Store Access Patterns** + +Three different patterns are used to update the same store: + +```typescript +// Pattern 1: Direct setState (handleStreamInitialization.ts) +useStreamingMessageStore.setState((prev) => ({ ... })); + +// Pattern 2: Passed update function (handleStreamPartBegin.ts) +updateStreamingMessage((prev) => ({ ... })); + +// Pattern 3: getState() then method call (handleIncompleteIndicator.ts) +useStreamingMessageStore.getState().setIncompleteIndicator(indicator); +``` + +**Problem:** Inconsistent patterns make it unclear which approach is correct and why. + +--- + +### 3. **Duplicated Type Checking Logic** + +The same exhaustive type-check pattern appears in 4+ files: + +```typescript +// Repeated in: useSendMessageStream.ts, handleStreamPartBegin.ts, handleStreamPartEnd.ts +if (role !== undefined) { + const _typeCheck: never = role; + throw new Error("Unexpected response payload: " + _typeCheck); +} +``` + +**Problem:** Duplication increases maintenance burden. If a new message type is added, 4+ files need updating. + +--- + +### 4. **Nearly Identical Switch Statements** + +`handleStreamPartBegin.ts` (lines 11-72) and `handleStreamPartEnd.ts` (lines 16-98) have almost identical structures: + +```typescript +// handleStreamPartBegin.ts +if (role === "assistant") { ... } +else if (role === "toolCallPrepareArguments") { ... } +else if (role === "toolCall") { ... } +else if (role === "system") { /* nothing */ } +else if (role === "user") { /* nothing */ } +else if (role === "unknown") { /* nothing */ } + +// handleStreamPartEnd.ts (same pattern with switch) +switch (role) { + case "assistant": { ... } + case "toolCallPrepareArguments": { ... } + case "toolCall": { ... } + case "system": { break; } + case "unknown": { break; } + case "user": { break; } +} +``` + +**Problem:** Changes to message type handling require modifications in multiple places. Uses different control flow structures (if-else vs switch) for the same logic. + +--- + +### 5. **Complex State Transitions Across Files** + +Message status transitions are implicit and distributed: + +``` +MessageEntryStatus Flow: + PREPARING → (in handleStreamPartBegin) + ↓ + PREPARING + content updates → (in handleMessageChunk) + ↓ + FINALIZED → (in handleStreamPartEnd or handleStreamInitialization) + ↓ + [flush to conversation store] → (in converter.ts) + +Error paths: + PREPARING → STALE → (in handleError.ts) +``` + +**Problem:** No single place documents or enforces valid state transitions. Easy to introduce bugs. + +--- + +### 6. **Dual Store Architecture Creates Complexity** + +Two stores manage related message data: + +| Store | Purpose | When Used | +|-------|---------|-----------| +| `streaming-message-store` | Temporary streaming state | During active streaming | +| `conversation-store` | Persisted conversation | After stream finalization | + +Data flows: +1. Messages created in `streaming-message-store` +2. Messages modified via handlers +3. Messages "flushed" to `conversation-store` via `flushStreamingMessageToConversation` +4. `streaming-message-store` is reset + +**Problem:** +- Two sources of truth for messages +- Complex flush logic (`flushSync` required for React batching) +- UI must render from both stores simultaneously + +--- + +### 7. **Multiple Data Transformations** + +A message goes through 5+ transformations: + +``` +StreamPartBegin (protobuf) + ↓ convert +MessageEntry (internal, PREPARING) + ↓ handleMessageChunk +MessageEntry (updated content) + ↓ handleStreamPartEnd +MessageEntry (FINALIZED) + ↓ convertMessageEntryToMessage +Message (protobuf) + ↓ messageToMessageEntry (for UI) +MessageEntry (for MessageCard component) +``` + +**Problem:** Each transformation is a potential source of bugs. Data shape changes make debugging harder. + +--- + +### 8. **flushSync Usage Indicates Architectural Issues** + +`flushSync` is used in 3 places to force synchronous React updates: + +```typescript +// streaming-message-store.ts +flushSync(() => { set((state) => { ... }); }); + +// converter.ts +flushSync(() => { useConversationStore.getState().updateCurrentConversation(...); }); +``` + +**Problem:** `flushSync` is a React escape hatch. Its presence suggests the architecture fights against React's batching model. + +--- + +### 9. **Error Handling Inconsistency** + +Three different error handling approaches: + +```typescript +// handleStreamError.ts - specific to stream errors with retry +if (streamError.errorMessage.includes("project is out of date")) { + await sync(); + await sendMessageStream(currentPrompt, currentSelectedText); +} + +// handleError.ts - marks messages as stale +useStreamingMessageStore.getState().updateStreamingMessage((prev) => { + const newParts = prev.parts.map((part) => ({ + ...part, + status: part.status === MessageEntryStatus.PREPARING ? MessageEntryStatus.STALE : part.status, + })); + return { ...prev, parts: newParts }; +}); + +// with-retry-sync.ts - wrapper with generic retry logic +if (error?.code === ErrorCode.PROJECT_OUT_OF_DATE) { + await sync(); + return await operation(); // retry once +} +``` + +**Problem:** Unclear which error handler is called when. Duplicate retry logic for the same error condition. + +--- + +### 10. **Hook Has Too Many Dependencies** + +`useSendMessageStream` has 12 dependencies in its `useCallback`: + +```typescript +[ + resetStreamingMessage, + resetIncompleteIndicator, + updateStreamingMessage, + currentConversation, + refetchConversationList, + sync, + user?.id, + alwaysSyncProject, + conversationMode, + storeSurroundingText, + projectId, +] +``` + +**Problem:** Hard to reason about when the callback is recreated. Potential performance issues if any dependency changes frequently. + +--- + +## Recommendations Summary + +1. **Consolidate handlers** into a single state machine +2. **Use a single store** with clear separation between streaming and finalized state +3. **Create a message type handler registry** to eliminate switch/if-else duplication +4. **Define explicit state transitions** with validation +5. **Remove flushSync** by restructuring the update flow +6. **Unify error handling** into a single strategy +7. **Reduce transformations** by using a consistent message format + +--- + +## TODO: Improve Streaming Design + +See the accompanying TODO list for specific implementation tasks. diff --git a/webapp/_webapp/docs/STREAMING_DESIGN_TODO.md b/webapp/_webapp/docs/STREAMING_DESIGN_TODO.md new file mode 100644 index 00000000..16b36d38 --- /dev/null +++ b/webapp/_webapp/docs/STREAMING_DESIGN_TODO.md @@ -0,0 +1,521 @@ +# TODO: Streaming Message Design Improvements + +This document outlines a phased approach to simplify and improve the front-end streaming architecture. + +--- + +## Phase 1: Consolidate Handlers into State Machine ✅ COMPLETED + +### Goal +Replace 9 separate handler files with a single, cohesive state machine that manages all streaming state transitions. + +### Tasks + +- [x] **1.1 Create StreamingStateMachine class** + - Location: `stores/streaming/streaming-state-machine.ts` + - Implemented as a Zustand store with `handleEvent` method for centralized event handling + - Benefit: Single point of control for all state transitions + +- [x] **1.2 Define StreamEvent union type** + - Location: `stores/streaming/types.ts` + - Includes all event types: INIT, PART_BEGIN, CHUNK, REASONING_CHUNK, PART_END, FINALIZE, ERROR, INCOMPLETE, CONNECTION_ERROR + - Benefit: Type-safe event handling with exhaustive checking + +- [x] **1.3 Create message type handlers registry** + - Location: `stores/streaming/message-type-handlers.ts` + - Implemented handlers: AssistantHandler, ToolCallHandler, ToolCallPrepareHandler, NoOpHandler + - Benefit: Add new message types without modifying multiple files + +- [x] **1.4 Delete old handler files** + - Deleted files: + - `handlers/handleStreamInitialization.ts` + - `handlers/handleStreamPartBegin.ts` + - `handlers/handleMessageChunk.ts` + - `handlers/handleReasoningChunk.ts` + - `handlers/handleStreamPartEnd.ts` + - `handlers/handleStreamFinalization.ts` + - `handlers/handleStreamError.ts` + - `handlers/handleIncompleteIndicator.ts` + - `handlers/handleError.ts` + - `handlers/converter.ts` + +### New File Structure + +``` +stores/streaming/ +├── index.ts # Module exports +├── types.ts # StreamEvent, MessageEntry, etc. +├── message-type-handlers.ts # Handler registry +└── streaming-state-machine.ts # Main state machine (Zustand store) +``` + +### Migration Notes + +- `streaming-message-store.ts` now serves as a backward compatibility layer +- `conversation/types.ts` re-exports types from the streaming module +- All consumer components updated to use `useStreamingStateMachine` + +--- + +## Phase 2: Unify Store Architecture ✅ COMPLETED + +### Goal +Consolidate `streaming-message-store` and `conversation-store` message handling into a single coherent store. + +### Tasks + +- [x] **2.1 Create unified message store** + - Location: `stores/message-store.ts` + - Implemented with automatic subscriptions to `conversation-store` and `streaming-state-machine` + - Provides `getAllDisplayMessages()` and `getVisibleDisplayMessages()` selectors + - Auto-initializes subscriptions on module import + - Benefit: Single source of truth with clear streaming vs finalized separation + +- [x] **2.2 Create DisplayMessage type** + - Location: `stores/types.ts` + - Implemented with types: `user`, `assistant`, `toolCall`, `toolCallPrepare`, `error` + - Status types: `streaming`, `complete`, `error`, `stale` + - Includes support for reasoning, tool args/results, and selected text + - Benefit: UI components work with one consistent type + +- [x] **2.3 Remove flushSync calls** + - No `flushSync` calls exist in the codebase + - Update flow uses Zustand's `subscribeWithSelector` middleware for reactive subscriptions + - `conversation-store` and `streaming-state-machine` now both use `subscribeWithSelector` + - `message-store` subscribes to both stores and updates automatically + +- [x] **2.4 Migrate ChatBody to use unified store** + - ChatBody now uses: + ```typescript + const allDisplayMessages = useMessageStore((s) => s.getAllDisplayMessages()); + ``` + - Helper functions updated to use unified store (e.g., `isEmptyConversation`) + - Converters in `stores/converters.ts` provide bidirectional conversion + +### Architecture Notes + +The unified message store architecture: + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ useMessageStore │ +│ - messages: Message[] (finalized) │ +│ - streamingEntries: InternalMessage[] (streaming) │ +│ - getAllDisplayMessages(): DisplayMessage[] │ +└─────────────────────────────┬───────────────────────────────┬──────────────┘ + │ │ + ┌─────────────────┴─────────────┐ ┌─────────────┴─────────────┐ + │ subscribes to │ │ subscribes to │ + ▼ │ ▼ │ +┌───────────────────────────────┐ │ ┌───────────────────────────┐ +│ useConversationStore │ │ │ useStreamingStateMachine │ +│ - currentConversation │ │ │ - streamingMessage │ +│ (finalized messages) │ │ │ (streaming InternalMessage) │ +└───────────────────────────────┘ │ └───────────────────────────┘ + │ + ▼ + ┌─────────────────────────────────┐ + │ ChatBody Component │ + │ Uses useMessageStore directly │ + └─────────────────────────────────┘ +``` + +--- + +## Phase 3: Simplify Data Transformations ✅ COMPLETED + +### Goal +Reduce the number of data transformations from 5+ to 2 maximum. + +### Tasks + +- [x] **3.1 Define canonical internal message format** + - Location: `types/message.ts` + - Implemented `InternalMessage` union type with role-specific subtypes: + - `UserMessage`, `AssistantMessage`, `ToolCallMessage`, `ToolCallPrepareMessage`, `SystemMessage`, `UnknownMessage` + - Added type guards: `isUserMessage()`, `isAssistantMessage()`, etc. + - Added factory functions: `createUserMessage()`, `createAssistantMessage()`, etc. + - Benefit: Single format with type-safe role-specific data access + +- [x] **3.2 Create bidirectional converters** + - Location: `utils/message-converters.ts` + - Implemented converters: + - `fromApiMessage()` - API Message → InternalMessage + - `toApiMessage()` - InternalMessage → API Message + - `fromStreamPartBegin()` - Stream event → InternalMessage + - `applyStreamPartEnd()` - Update InternalMessage from stream end event + - `toDisplayMessage()` / `fromDisplayMessage()` - InternalMessage ↔ DisplayMessage + - Benefit: Clear boundary between API types and internal types + +- [x] **3.3 Update MessageCard to use DisplayMessage directly** + - MessageCard now accepts `message: DisplayMessage` prop instead of `messageEntry: MessageEntry` + - Removed `displayMessageToMessageEntry()` bridge function from helper.ts + - ChatBody passes DisplayMessage directly to MessageCard + - Benefit: Eliminated unnecessary data transformation at render time + +- [x] **3.4 Remove legacy MessageEntry type** + - Streaming state machine now uses `InternalMessage` directly instead of `MessageEntry` + - Removed `MessageEntry` and `MessageEntryStatus` enum from `streaming/types.ts` + - Updated message-store.ts to use `streamingEntries: InternalMessage[]` + - Removed legacy converters (`fromMessageEntry`, `toMessageEntry`) + - Updated all consumers (hooks, views, devtools) to use `InternalMessage` and `MessageStatus` + +### New File Structure + +``` +types/ +├── index.ts # Module exports for types +├── message.ts # InternalMessage type definitions +└── global.d.ts # (existing) + +utils/ +├── index.ts # Module exports for utilities +└── message-converters.ts # All message converters in one place +``` + +### Data Flow After Phase 3 + +``` +API Response (Message) + │ + ▼ fromApiMessage() +InternalMessage + │ + ▼ toDisplayMessage() +DisplayMessage ─────────────────────────► MessageCard + ▲ + │ (streaming state uses InternalMessage directly) +InternalMessage (streaming state) +``` + +### Migration Notes + +- Legacy `MessageEntry` type has been removed - all code uses `InternalMessage` +- `MessageEntryStatus` enum replaced with `MessageStatus` string union: `"streaming" | "complete" | "error" | "stale"` +- `stores/converters.ts` simplified to only bridge between API types and display types +- Factory functions (`createUserMessage`, etc.) used for creating new messages + +--- + +## Phase 4: Improve Error Handling ✅ COMPLETED + +### Goal +Create a unified error handling strategy for all streaming errors. + +### Tasks + +- [x] **4.1 Create StreamingErrorHandler class** + - Location: `stores/streaming/error-handler.ts` + - Implemented `StreamingErrorHandler` class with: + - `handle()` method for centralized error handling + - Support for multiple recovery strategies + - Exponential and linear backoff for retries + - Automatic error categorization from error messages and codes + - Benefit: Centralized error handling logic + +- [x] **4.2 Define error recovery strategies** + - Location: `stores/streaming/types.ts` + - Implemented `RecoveryStrategy` union type with: + - `retry` - Retry with configurable attempts and backoff + - `sync-and-retry` - Sync project then retry (for PROJECT_OUT_OF_DATE) + - `show-error` - Display error to user + - `abort` - Stop processing + - Added `StreamingError`, `ErrorContext`, `ErrorResolution` types + - Benefit: Explicit, testable recovery strategies + +- [x] **4.3 Remove duplicate retry logic** + - Created `withStreamingErrorHandler()` as replacement for `withRetrySync()` + - Updated `useSendMessageStream` to use new error handler + - Updated `streaming-state-machine.ts` to use `StreamingErrorHandler` + - Deprecated `with-retry-sync.ts` with migration guide + - Single retry implementation with configurable strategies + +### New File Structure + +``` +stores/streaming/ +├── index.ts # Module exports (updated) +├── types.ts # Added error handling types +├── message-type-handlers.ts # (existing) +├── streaming-state-machine.ts # Updated to use error handler +└── error-handler.ts # NEW: Centralized error handling +``` + +### Error Handling Flow + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ StreamingErrorHandler │ +│ - createStreamingError(): Normalize errors to StreamingError │ +│ - getRecoveryStrategy(): Get strategy based on error code │ +│ - handle(): Execute recovery strategy │ +└─────────────────────────────────┬──────────────────────────────────────────┘ + │ + ┌─────────────────────────────┼─────────────────────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ +│ retry │ │ sync-and-retry │ │ show-error │ +│ (NETWORK, │ │ (PROJECT_OUT_ │ │ (AUTH, INVALID, │ +│ TIMEOUT, etc.) │ │ OF_DATE) │ │ UNKNOWN) │ +└─────────────────┘ └─────────────────┘ └─────────────────────┘ +``` + +### Recovery Strategies Configuration + +| Error Code | Strategy | Max Attempts | Backoff | +|------------|----------|--------------|---------| +| PROJECT_OUT_OF_DATE | sync-and-retry | 2 | - | +| NETWORK_ERROR | retry | 3 | exponential, 1000ms | +| TIMEOUT | retry | 2 | linear, 2000ms | +| RATE_LIMITED | retry | 3 | exponential, 5000ms | +| SERVER_ERROR | retry | 2 | exponential, 2000ms | +| INVALID_RESPONSE | show-error | - | - | +| AUTHENTICATION_ERROR | show-error | - | - | +| UNKNOWN | show-error | - | - | + +### Migration Notes + +- `withRetrySync()` is deprecated, use `withStreamingErrorHandler()` instead +- Error handling in `streaming-state-machine.ts` now uses `StreamingErrorHandler` +- All error types are normalized to `StreamingError` for consistent handling +- Recovery strategies are configurable per error type + +--- + +## Phase 5: Refactor useSendMessageStream Hook ✅ COMPLETED + +### Goal +Simplify the main orchestration hook by delegating to the state machine. + +### Tasks + +- [x] **5.1 Simplify hook to single responsibility** + - Refactored hook to focus on orchestration + - Delegated all event handling to the state machine + - Added `isStreaming` state to return value for consumers + - Extracted helper functions (`addUserMessageToStream`, `truncateConversationIfEditing`) + - Benefit: Hook focuses on orchestration, not event handling + +- [x] **5.2 Reduce hook dependencies** + - Reduced from 12 dependencies to better organized structure + - Used `useCallback` for helper functions to stabilize references + - Used memoized return value with `useMemo` + - Improved store access patterns (using selectors instead of destructuring) + +- [x] **5.3 Extract request building logic** + - Location: `utils/stream-request-builder.ts` + - Created `buildStreamRequest()` function + - Added `validateStreamRequestParams()` for input validation + - Created `StreamRequestParams` interface for type safety + - Benefit: Testable, pure function for request creation + +- [x] **5.4 Extract response-to-event mapping** + - Location: `utils/stream-event-mapper.ts` + - Created `mapResponseToStreamEvent()` function + - Added type guards: `isFinalizeEvent`, `isErrorEvent`, `isInitEvent`, `isChunkEvent` + - Benefit: Pure function, easy to test and reuse + +### New File Structure + +``` +utils/ +├── index.ts # Updated exports +├── message-converters.ts # (existing) +├── stream-request-builder.ts # NEW: Request building logic +└── stream-event-mapper.ts # NEW: Response to event mapping + +hooks/ +└── useSendMessageStream.ts # REFACTORED: Simplified orchestration +``` + +### Architecture After Phase 5 + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ useSendMessageStream Hook │ +│ (Orchestrator - single responsibility) │ +└─────────────────────────────────┬──────────────────────────────────────────┘ + │ + ┌─────────────────────────────┼─────────────────────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ +│ buildStream │ │ mapResponseTo │ │ StreamingState │ +│ Request() │ │ StreamEvent() │ │ Machine │ +│ (pure function) │ │ (pure function) │ │ (event handling) │ +└─────────────────┘ └─────────────────┘ └─────────────────────┘ +``` + +### Hook Return Type + +```typescript +interface UseSendMessageStreamResult { + /** Function to send a message as a stream */ + sendMessageStream: (message: string, selectedText: string, parentMessageId?: string) => Promise; + /** Whether a stream is currently active */ + isStreaming: boolean; +} +``` + +### Migration Notes + +- Hook now returns `{ sendMessageStream, isStreaming }` instead of just `{ sendMessageStream }` +- No breaking changes to existing consumers (they can ignore `isStreaming` if not needed) +- Request building and event mapping are now testable pure functions +- Error handling uses `withStreamingErrorHandler` from Phase 4 + +--- + +## Phase 6: Testing & Documentation ✅ COMPLETED + +### Goal +Ensure the refactored code is well-tested and documented. + +### Tasks + +- [x] **6.1 Add unit tests for state machine** + - Location: `stores/streaming/__tests__/streaming-state-machine.test.ts` + - Tests all state transitions: idle → receiving → finalizing → idle + - Tests error handling: ERROR, CONNECTION_ERROR events + - Tests all event types: INIT, PART_BEGIN, CHUNK, REASONING_CHUNK, PART_END, FINALIZE, INCOMPLETE + - Tests message type handlers via state machine integration + +- [x] **6.2 Add unit tests for message type handlers** + - Location: `stores/streaming/__tests__/message-type-handlers.test.ts` + - Tests handler registry and `getMessageTypeHandler()` + - Tests `isValidMessageRole()` type guard + - Tests AssistantHandler: onPartBegin, onPartEnd + - Tests ToolCallPrepareHandler: onPartBegin, onPartEnd + - Tests ToolCallHandler: onPartBegin, onPartEnd + - Tests NoOpHandler for user, system, unknown roles + +- [x] **6.3 Add unit tests for error handler** + - Location: `stores/streaming/__tests__/error-handler.test.ts` + - Tests `createStreamingError()` for all error sources + - Tests `getRecoveryStrategy()` for all error codes + - Tests `isRetryableError()` helper + - Tests `StreamingErrorHandler` class with retry, sync-and-retry, show-error strategies + - Tests `withStreamingErrorHandler()` wrapper function + - Tests backoff calculation (exponential and linear) + +- [x] **6.4 Add unit tests for message converters** + - Location: `utils/__tests__/message-converters.test.ts` + - Tests `fromApiMessage()` for all message types + - Tests `toApiMessage()` for all message types + - Tests `fromStreamPartBegin()` and `applyStreamPartEnd()` + - Tests `toDisplayMessage()` and `fromDisplayMessage()` + - Tests round-trip conversion integrity + +- [x] **6.5 Add integration tests for streaming flow** + - Location: `src/__tests__/streaming-flow.integration.test.ts` + - Tests stream request building and validation + - Tests stream event mapping for all response types + - Tests complete happy path: INIT → PART_BEGIN → CHUNK → PART_END → FINALIZE + - Tests tool call flow with prepare and result messages + - Tests reasoning chunk handling + - Tests error scenarios: stream error, connection error + - Tests state transitions and sequence number management + +- [x] **6.6 Document the new architecture** + - Location: `docs/STREAMING_ARCHITECTURE.md` + - Complete architecture diagram with all components + - Core components documentation (StreamingStateMachine, MessageTypeHandlers, ErrorHandler, MessageStore) + - Data types documentation (InternalMessage, DisplayMessage) + - Data flow diagrams (happy path, error recovery) + - File structure overview + - Extension points for adding new message types and error types + - Testing instructions + - Performance considerations + - Troubleshooting guide + +- [x] **6.7 Create migration guide** + - Location: `docs/STREAMING_MIGRATION_GUIDE.md` + - Summary of breaking changes + - MessageEntry → InternalMessage migration + - Handler files → State machine migration + - withRetrySync → withStreamingErrorHandler migration + - Dual store access → MessageStore migration + - MessageCard props migration + - File changes (removed/added) + - Import updates + - Common migration patterns with code examples + - Migration checklist + +### Test File Structure + +``` +src/ +├── stores/streaming/__tests__/ +│ ├── streaming-state-machine.test.ts # State machine unit tests +│ ├── message-type-handlers.test.ts # Handler registry tests +│ └── error-handler.test.ts # Error handling tests +├── utils/__tests__/ +│ └── message-converters.test.ts # Converter tests +└── __tests__/ + └── streaming-flow.integration.test.ts # Integration tests +``` + +### Documentation Structure + +``` +docs/ +├── STREAMING_DESIGN_ANALYSIS.md # Original complexity analysis +├── STREAMING_DESIGN_TODO.md # This file - implementation checklist +├── STREAMING_ARCHITECTURE.md # NEW: Architecture documentation +└── STREAMING_MIGRATION_GUIDE.md # NEW: Developer migration guide +``` + +--- + +## Implementation Priority + +| Phase | Priority | Effort | Impact | Status | +|-------|----------|--------|--------|--------| +| 1. Consolidate Handlers | High | Medium | High | ✅ COMPLETED | +| 2. Unify Stores | High | High | High | ✅ COMPLETED | +| 3. Simplify Transformations | Medium | Medium | Medium | ✅ COMPLETED | +| 4. Error Handling | Medium | Low | Medium | ✅ COMPLETED | +| 5. Refactor Hook | Low | Low | Medium | ✅ COMPLETED | +| 6. Testing & Docs | Low | Medium | High | ✅ COMPLETED | + +--- + +## Success Metrics + +After completing all phases: + +- [x] Single source of truth for message state (Phase 2) +- [x] No `flushSync` calls required (Phase 2) +- [x] All state transitions documented and validated (Phase 1) +- [x] Adding a new message type requires changes to only 1-2 files (Phase 1) +- [x] Canonical internal message format defined (Phase 3) +- [x] Bidirectional converters centralized in one file (Phase 3) +- [x] MessageCard uses DisplayMessage directly (Phase 3) +- [x] Centralized error handling with configurable strategies (Phase 4) +- [x] Single retry implementation with backoff support (Phase 4) +- [x] Error types normalized for consistent handling (Phase 4) +- [x] Hook has single responsibility (orchestration only) (Phase 5) +- [x] Request building extracted to pure function (Phase 5) +- [x] Event mapping extracted to pure function (Phase 5) +- [x] Unit tests for state machine, handlers, error handler, converters (Phase 6) +- [x] Integration tests for complete streaming flow (Phase 6) +- [x] Architecture documentation with diagrams (Phase 6) +- [x] Migration guide with code examples (Phase 6) + +--- + +## 🎉 All Phases Complete! + +The streaming architecture refactoring is now complete. All 6 phases have been implemented: + +1. ✅ Consolidated 9+ handler files into a single state machine +2. ✅ Unified dual store architecture into MessageStore +3. ✅ Simplified data transformations with canonical InternalMessage type +4. ✅ Centralized error handling with configurable recovery strategies +5. ✅ Refactored useSendMessageStream hook for single responsibility +6. ✅ Added comprehensive tests and documentation + +For details on the new architecture, see: +- [STREAMING_ARCHITECTURE.md](./STREAMING_ARCHITECTURE.md) - Technical documentation +- [STREAMING_MIGRATION_GUIDE.md](./STREAMING_MIGRATION_GUIDE.md) - Migration instructions diff --git a/webapp/_webapp/docs/STREAMING_MIGRATION_GUIDE.md b/webapp/_webapp/docs/STREAMING_MIGRATION_GUIDE.md new file mode 100644 index 00000000..72cfc24e --- /dev/null +++ b/webapp/_webapp/docs/STREAMING_MIGRATION_GUIDE.md @@ -0,0 +1,429 @@ +# Migration Guide: Streaming Architecture Refactoring + +This guide documents the changes made during the streaming architecture refactoring and provides migration instructions for developers working with the codebase. + +## Summary of Changes + +The streaming implementation has been refactored from 9+ fragmented handler files into a consolidated architecture with: + +- **Single State Machine**: Centralized event handling +- **Unified Message Store**: Single source of truth for messages +- **Simplified Data Types**: Canonical `InternalMessage` format +- **Centralized Error Handling**: Configurable recovery strategies + +## Breaking Changes + +### 1. MessageEntry → InternalMessage + +**Old Type (Removed):** +```typescript +// ❌ REMOVED +interface MessageEntry { + id: string; + role: MessageEntryRole; + status: MessageEntryStatus; + content: string; + selectedText?: string; + reasoning?: string; + toolArgs?: string; + toolResults?: string; + // ... +} + +enum MessageEntryStatus { + PREPARING = "preparing", + FINALIZED = "finalized", + STALE = "stale", +} +``` + +**New Type:** +```typescript +// ✅ USE THIS +type InternalMessage = + | UserMessage + | AssistantMessage + | ToolCallMessage + | ToolCallPrepareMessage + | SystemMessage + | UnknownMessage; + +type MessageStatus = "streaming" | "complete" | "error" | "stale"; + +interface AssistantMessage { + id: string; + role: "assistant"; + status: MessageStatus; + data: { + content: string; + reasoning?: string; + modelSlug?: string; + }; +} +``` + +**Migration:** +```typescript +// Before +import { MessageEntry, MessageEntryStatus } from "./stores/conversation/types"; + +function processEntry(entry: MessageEntry) { + if (entry.status === MessageEntryStatus.PREPARING) { + // ... + } + const content = entry.content; +} + +// After +import { InternalMessage, isAssistantMessage } from "./types/message"; + +function processEntry(msg: InternalMessage) { + if (msg.status === "streaming") { + // ... + } + if (isAssistantMessage(msg)) { + const content = msg.data.content; + } +} +``` + +### 2. Handler Files → State Machine + +**Old Pattern (Removed):** +```typescript +// ❌ REMOVED - Multiple handler files +import { handleStreamPartBegin } from "./handlers/handleStreamPartBegin"; +import { handleMessageChunk } from "./handlers/handleMessageChunk"; +import { handleStreamPartEnd } from "./handlers/handleStreamPartEnd"; + +// Called from useSendMessageStream +handleStreamPartBegin(payload, updateStreamingMessage); +handleMessageChunk(payload, updateStreamingMessage); +handleStreamPartEnd(payload, updateStreamingMessage); +``` + +**New Pattern:** +```typescript +// ✅ USE THIS +import { useStreamingStateMachine } from "./stores/streaming"; + +// In useSendMessageStream +const event = mapResponseToStreamEvent(response); +if (event) { + await stateMachine.handleEvent(event, context); +} +``` + +### 3. withRetrySync → withStreamingErrorHandler + +**Old Pattern (Deprecated):** +```typescript +// ⚠️ DEPRECATED +import { withRetrySync } from "./libs/with-retry-sync"; + +await withRetrySync( + () => sendMessage(), + sync, + onGiveUp +); +``` + +**New Pattern:** +```typescript +// ✅ USE THIS +import { withStreamingErrorHandler } from "./stores/streaming"; + +await withStreamingErrorHandler( + () => sendMessage(), + { + sync: async () => { + const result = await sync(); + return result; + }, + onGiveUp: () => { + // Handle failure + }, + context: { + currentPrompt: prompt, + currentSelectedText: selectedText, + operation: "send-message", + }, + } +); +``` + +### 4. Dual Store Access → MessageStore + +**Old Pattern:** +```typescript +// ❌ AVOID +import { useConversationStore } from "./stores/conversation/conversation-store"; +import { useStreamingMessageStore } from "./stores/streaming-message-store"; + +function ChatBody() { + const messages = useConversationStore((s) => s.currentConversation.messages); + const streamingMessage = useStreamingMessageStore((s) => s.streamingMessage); + + // Manually combine finalized and streaming messages + const allMessages = [...messages, ...streamingMessage.parts]; +} +``` + +**New Pattern:** +```typescript +// ✅ USE THIS +import { useMessageStore } from "./stores/message-store"; + +function ChatBody() { + const allMessages = useMessageStore((s) => s.getAllDisplayMessages()); + // Already combined and converted to DisplayMessage +} +``` + +### 5. MessageCard Props + +**Old Props:** +```typescript +// ❌ OLD +interface MessageCardProps { + messageEntry: MessageEntry; + // ... +} +``` + +**New Props:** +```typescript +// ✅ NEW +interface MessageCardProps { + message: DisplayMessage; + // ... +} +``` + +**Migration:** +```typescript +// Before + + +// After + +``` + +## File Changes + +### Removed Files + +The following handler files have been removed: + +- `stores/conversation/handlers/handleStreamInitialization.ts` +- `stores/conversation/handlers/handleStreamPartBegin.ts` +- `stores/conversation/handlers/handleMessageChunk.ts` +- `stores/conversation/handlers/handleReasoningChunk.ts` +- `stores/conversation/handlers/handleStreamPartEnd.ts` +- `stores/conversation/handlers/handleStreamFinalization.ts` +- `stores/conversation/handlers/handleStreamError.ts` +- `stores/conversation/handlers/handleIncompleteIndicator.ts` +- `stores/conversation/handlers/handleError.ts` +- `stores/conversation/handlers/converter.ts` + +### New Files + +The following files have been added: + +``` +stores/streaming/ +├── index.ts # Module exports +├── types.ts # Type definitions +├── streaming-state-machine.ts # Main state machine +├── message-type-handlers.ts # Handler registry +├── error-handler.ts # Error handling + +types/ +└── message.ts # InternalMessage types + +utils/ +├── message-converters.ts # Bidirectional converters +├── stream-request-builder.ts # Request building +└── stream-event-mapper.ts # Response → Event mapping +``` + +## Import Updates + +### Old Imports (Update These) + +```typescript +// ❌ REMOVE +import { MessageEntry, MessageEntryStatus } from "./stores/conversation/types"; +import { useStreamingMessageStore } from "./stores/streaming-message-store"; +import { withRetrySync } from "./libs/with-retry-sync"; +``` + +### New Imports + +```typescript +// ✅ ADD +import { InternalMessage, MessageStatus } from "./types/message"; +import { + useStreamingStateMachine, + withStreamingErrorHandler, + StreamEvent, +} from "./stores/streaming"; +import { useMessageStore } from "./stores/message-store"; +import { DisplayMessage } from "./stores/types"; +``` + +## Common Migration Patterns + +### Pattern 1: Checking Message Status + +```typescript +// Before +if (entry.status === MessageEntryStatus.PREPARING) { + // streaming +} else if (entry.status === MessageEntryStatus.FINALIZED) { + // complete +} else if (entry.status === MessageEntryStatus.STALE) { + // stale +} + +// After +if (msg.status === "streaming") { + // streaming +} else if (msg.status === "complete") { + // complete +} else if (msg.status === "stale") { + // stale +} +``` + +### Pattern 2: Accessing Message Content + +```typescript +// Before +const content = entry.content; +const reasoning = entry.reasoning; +const selectedText = entry.selectedText; + +// After +if (isAssistantMessage(msg)) { + const content = msg.data.content; + const reasoning = msg.data.reasoning; +} +if (isUserMessage(msg)) { + const content = msg.data.content; + const selectedText = msg.data.selectedText; +} +``` + +### Pattern 3: Creating Messages + +```typescript +// Before +const entry: MessageEntry = { + id: "msg-1", + role: "assistant", + status: MessageEntryStatus.PREPARING, + content: "Hello", + reasoning: "", +}; + +// After +import { createAssistantMessage } from "./types/message"; + +const msg = createAssistantMessage("msg-1", "Hello", { + status: "streaming", + reasoning: "", +}); +``` + +### Pattern 4: Converting for API + +```typescript +// Before +import { messageToEntry, entryToMessage } from "./utils/converters"; + +const entry = messageToEntry(apiMessage); +const message = entryToMessage(entry); + +// After +import { fromApiMessage, toApiMessage } from "./utils/message-converters"; + +const internal = fromApiMessage(apiMessage); +const message = toApiMessage(internal); +``` + +## Testing Changes + +### Update Test Mocks + +```typescript +// Before +const mockEntry: MessageEntry = { + id: "test-1", + role: "assistant", + status: MessageEntryStatus.FINALIZED, + content: "Test", +}; + +// After +const mockMessage: InternalMessage = { + id: "test-1", + role: "assistant", + status: "complete", + data: { + content: "Test", + }, +}; +``` + +### Update Test Assertions + +```typescript +// Before +expect(entry.status).toBe(MessageEntryStatus.PREPARING); + +// After +expect(msg.status).toBe("streaming"); +``` + +## Backward Compatibility + +### streaming-message-store.ts + +The `streaming-message-store.ts` file has been kept as a thin backward compatibility layer. It delegates to the new `useStreamingStateMachine` store. If you have code using this store, it will continue to work but should be migrated. + +```typescript +// Still works but deprecated +import { useStreamingMessageStore } from "./stores/streaming-message-store"; + +// Preferred +import { useStreamingStateMachine } from "./stores/streaming"; +``` + +### Type Re-exports + +For convenience, some types are re-exported: + +```typescript +// stores/streaming/types.ts re-exports InternalMessage +export type { InternalMessage, MessageStatus } from "../../types/message"; + +// stores/conversation/types.ts re-exports for compatibility +export type { InternalMessage } from "../streaming/types"; +``` + +## Checklist for Migration + +- [ ] Update imports from removed handler files +- [ ] Replace `MessageEntry` with `InternalMessage` +- [ ] Replace `MessageEntryStatus` enum with `MessageStatus` string literals +- [ ] Update message content access to use `.data` property +- [ ] Replace `withRetrySync` with `withStreamingErrorHandler` +- [ ] Use `useMessageStore` instead of dual store access +- [ ] Update component props from `messageEntry` to `message` +- [ ] Run tests to verify functionality +- [ ] Remove any unused imports + +## Questions? + +Refer to the [Architecture Documentation](./STREAMING_ARCHITECTURE.md) for detailed explanations of the new system. diff --git a/webapp/_webapp/eslint.config.js b/webapp/_webapp/eslint.config.js index bdeade8b..c9e0aaed 100644 --- a/webapp/_webapp/eslint.config.js +++ b/webapp/_webapp/eslint.config.js @@ -21,6 +21,14 @@ export default tseslint.config( ...reactHooks.configs.recommended.rules, "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], "no-console": "error", + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], }, }, ); diff --git a/webapp/_webapp/package.json b/webapp/_webapp/package.json index faf1f0a1..cbb93b1f 100644 --- a/webapp/_webapp/package.json +++ b/webapp/_webapp/package.json @@ -31,6 +31,10 @@ "@iconify/react": "^6.0.0", "@lukemorales/query-key-factory": "^1.3.4", "@r2wc/react-to-web-component": "^2.1.0", + "@streamdown/cjk": "^1.0.1", + "@streamdown/code": "^1.0.1", + "@streamdown/math": "^1.0.1", + "@streamdown/mermaid": "^1.0.1", "@tanstack/react-query": "^5.79.0", "@types/diff": "^8.0.0", "@uidotdev/usehooks": "^2.4.1", @@ -50,6 +54,7 @@ "react-dom": "^19.1.0", "react-rnd": "^10.5.2", "semver": "^7.7.2", + "streamdown": "^2.1.0", "uuid": "^11.1.0", "zustand": "^5.0.5" }, diff --git a/webapp/_webapp/src/__tests__/streaming-flow.integration.test.ts b/webapp/_webapp/src/__tests__/streaming-flow.integration.test.ts new file mode 100644 index 00000000..8ed83d8a --- /dev/null +++ b/webapp/_webapp/src/__tests__/streaming-flow.integration.test.ts @@ -0,0 +1,529 @@ +/** + * Integration Tests for Streaming Flow + * + * Tests the complete streaming flow from request to UI update, + * including happy paths and error scenarios. + */ + +import { describe, it, expect, beforeEach, mock } from "bun:test"; +import { useStreamingStateMachine } from "../stores/streaming/streaming-state-machine"; +import { useMessageStore } from "../stores/message-store"; +import { mapResponseToStreamEvent } from "../utils/stream-event-mapper"; +import { + buildStreamRequest, + validateStreamRequestParams, +} from "../utils/stream-request-builder"; +import { StreamEvent } from "../stores/streaming/types"; + +// Mock external dependencies +mock.module("../stores/conversation/conversation-store", () => ({ + useConversationStore: { + getState: () => ({ + updateCurrentConversation: mock(() => {}), + currentConversation: { messages: [], id: "", modelSlug: "" }, + }), + subscribe: mock(() => () => {}), + }, +})); + +mock.module("../libs/logger", () => ({ + logError: mock(() => {}), + logWarn: mock(() => {}), + logInfo: mock(() => {}), +})); + +mock.module("../query/api", () => ({ + getConversation: mock(async () => ({ conversation: null })), +})); + +describe("Streaming Flow Integration", () => { + beforeEach(() => { + useStreamingStateMachine.getState().reset(); + }); + + describe("Stream Request Building", () => { + it("should build valid request from parameters", () => { + const params = { + message: "Hello", + selectedText: "some text", + projectId: "proj-123", + conversationId: "conv-456", + modelSlug: "gpt-4", + conversationMode: "default" as const, + }; + + const request = buildStreamRequest(params); + + expect(request.projectId).toBe("proj-123"); + expect(request.conversationId).toBe("conv-456"); + expect(request.modelSlug).toBe("gpt-4"); + expect(request.userMessage).toBe("Hello"); + expect(request.userSelectedText).toBe("some text"); + }); + + it("should validate required parameters", () => { + // Missing message + expect(validateStreamRequestParams({ projectId: "1", conversationId: "2", modelSlug: "3" }).valid).toBe(false); + + // Missing projectId + expect(validateStreamRequestParams({ message: "hi", conversationId: "2", modelSlug: "3" }).valid).toBe(false); + + // Valid parameters + expect( + validateStreamRequestParams({ + message: "Hello", + projectId: "proj-123", + conversationId: "conv-456", + modelSlug: "gpt-4", + }).valid + ).toBe(true); + }); + }); + + describe("Stream Event Mapping", () => { + it("should map streamInitialization response", () => { + const response = { + responsePayload: { + case: "streamInitialization", + value: { conversationId: "conv-123", modelSlug: "gpt-4" }, + }, + }; + + const event = mapResponseToStreamEvent(response as any); + + expect(event).not.toBeNull(); + expect(event!.type).toBe("INIT"); + }); + + it("should map streamPartBegin response", () => { + const response = { + responsePayload: { + case: "streamPartBegin", + value: { + messageId: "msg-1", + payload: { + messageType: { + case: "assistant", + value: { content: "" }, + }, + }, + }, + }, + }; + + const event = mapResponseToStreamEvent(response as any); + + expect(event).not.toBeNull(); + expect(event!.type).toBe("PART_BEGIN"); + }); + + it("should map messageChunk response", () => { + const response = { + responsePayload: { + case: "messageChunk", + value: { messageId: "msg-1", delta: "Hello" }, + }, + }; + + const event = mapResponseToStreamEvent(response as any); + + expect(event).not.toBeNull(); + expect(event!.type).toBe("CHUNK"); + }); + + it("should map streamFinalization response", () => { + const response = { + responsePayload: { + case: "streamFinalization", + value: { conversationId: "conv-123" }, + }, + }; + + const event = mapResponseToStreamEvent(response as any); + + expect(event).not.toBeNull(); + expect(event!.type).toBe("FINALIZE"); + }); + + it("should map streamError response", () => { + const response = { + responsePayload: { + case: "streamError", + value: { errorMessage: "Something went wrong" }, + }, + }; + + const event = mapResponseToStreamEvent(response as any); + + expect(event).not.toBeNull(); + expect(event!.type).toBe("ERROR"); + }); + + it("should return null for undefined payload", () => { + const response = { + responsePayload: { + case: undefined, + value: undefined, + }, + }; + + const event = mapResponseToStreamEvent(response as any); + expect(event).toBeNull(); + }); + }); + + describe("Complete Streaming Flow", () => { + it("should handle happy path: INIT → PART_BEGIN → CHUNK → PART_END → FINALIZE", async () => { + const stateMachine = useStreamingStateMachine.getState(); + + // Add user message first + useStreamingStateMachine.setState({ + streamingMessage: { + parts: [ + { + id: "user-1", + role: "user", + status: "streaming", + data: { content: "Hello" }, + }, + ], + sequence: 1, + }, + }); + + // 1. INIT - Server acknowledges user message + await stateMachine.handleEvent({ + type: "INIT", + payload: { conversationId: "conv-123", modelSlug: "gpt-4" } as any, + }); + + expect(useStreamingStateMachine.getState().state).toBe("receiving"); + + // 2. PART_BEGIN - Start assistant response + await stateMachine.handleEvent({ + type: "PART_BEGIN", + payload: { + messageId: "assistant-1", + payload: { + messageType: { + case: "assistant", + value: { content: "", reasoning: "", modelSlug: "gpt-4" }, + }, + }, + } as any, + }); + + const afterBegin = useStreamingStateMachine.getState(); + expect(afterBegin.streamingMessage.parts).toHaveLength(1); + expect(afterBegin.streamingMessage.parts[0].role).toBe("assistant"); + + // 3. CHUNK - Stream content + await stateMachine.handleEvent({ + type: "CHUNK", + payload: { messageId: "assistant-1", delta: "Hello " } as any, + }); + + await stateMachine.handleEvent({ + type: "CHUNK", + payload: { messageId: "assistant-1", delta: "World!" } as any, + }); + + const afterChunks = useStreamingStateMachine.getState(); + const assistantMsg = afterChunks.streamingMessage.parts[0]; + if (assistantMsg.role === "assistant") { + expect(assistantMsg.data.content).toBe("Hello World!"); + } + + // 4. PART_END - Complete assistant message + await stateMachine.handleEvent({ + type: "PART_END", + payload: { + messageId: "assistant-1", + payload: { + messageType: { + case: "assistant", + value: { content: "Hello World!", reasoning: "", modelSlug: "gpt-4" }, + }, + }, + } as any, + }); + + const afterEnd = useStreamingStateMachine.getState(); + expect(afterEnd.streamingMessage.parts[0].status).toBe("complete"); + + // 5. FINALIZE - Stream complete + await stateMachine.handleEvent({ + type: "FINALIZE", + payload: { conversationId: "conv-123" } as any, + }); + + const finalState = useStreamingStateMachine.getState(); + expect(finalState.state).toBe("idle"); + expect(finalState.streamingMessage.parts).toEqual([]); + }); + + it("should handle tool call flow", async () => { + const stateMachine = useStreamingStateMachine.getState(); + + // PART_BEGIN for tool call prepare + await stateMachine.handleEvent({ + type: "PART_BEGIN", + payload: { + messageId: "tool-prep-1", + payload: { + messageType: { + case: "toolCallPrepareArguments", + value: { name: "search", args: "" }, + }, + }, + } as any, + }); + + expect(useStreamingStateMachine.getState().streamingMessage.parts[0].role).toBe( + "toolCallPrepare" + ); + + // PART_END for tool call prepare + await stateMachine.handleEvent({ + type: "PART_END", + payload: { + messageId: "tool-prep-1", + payload: { + messageType: { + case: "toolCallPrepareArguments", + value: { name: "search", args: '{"query": "test"}' }, + }, + }, + } as any, + }); + + // PART_BEGIN for tool call result + await stateMachine.handleEvent({ + type: "PART_BEGIN", + payload: { + messageId: "tool-1", + payload: { + messageType: { + case: "toolCall", + value: { name: "search", args: '{"query": "test"}', result: "", error: "" }, + }, + }, + } as any, + }); + + expect(useStreamingStateMachine.getState().streamingMessage.parts).toHaveLength(2); + + // PART_END for tool call + await stateMachine.handleEvent({ + type: "PART_END", + payload: { + messageId: "tool-1", + payload: { + messageType: { + case: "toolCall", + value: { + name: "search", + args: '{"query": "test"}', + result: "Found 3 results", + error: "", + }, + }, + }, + } as any, + }); + + const toolCallMsg = useStreamingStateMachine.getState().streamingMessage.parts[1]; + if (toolCallMsg.role === "toolCall") { + expect(toolCallMsg.data.result).toBe("Found 3 results"); + } + }); + + it("should handle reasoning chunks", async () => { + const stateMachine = useStreamingStateMachine.getState(); + + // Start assistant message + await stateMachine.handleEvent({ + type: "PART_BEGIN", + payload: { + messageId: "assistant-1", + payload: { + messageType: { + case: "assistant", + value: { content: "", reasoning: "", modelSlug: "gpt-4" }, + }, + }, + } as any, + }); + + // Send reasoning chunks + await stateMachine.handleEvent({ + type: "REASONING_CHUNK", + payload: { messageId: "assistant-1", delta: "Let me think" } as any, + }); + + await stateMachine.handleEvent({ + type: "REASONING_CHUNK", + payload: { messageId: "assistant-1", delta: " about this..." } as any, + }); + + const msg = useStreamingStateMachine.getState().streamingMessage.parts[0]; + if (msg.role === "assistant") { + expect(msg.data.reasoning).toBe("Let me think about this..."); + } + }); + }); + + describe("Error Scenarios", () => { + it("should handle stream error", async () => { + const stateMachine = useStreamingStateMachine.getState(); + + await stateMachine.handleEvent({ + type: "ERROR", + payload: { errorMessage: "Rate limit exceeded" } as any, + }); + + const state = useStreamingStateMachine.getState(); + expect(state.state).toBe("error"); + }); + + it("should handle connection error", async () => { + const stateMachine = useStreamingStateMachine.getState(); + + // Add a streaming message first + await stateMachine.handleEvent({ + type: "PART_BEGIN", + payload: { + messageId: "msg-1", + payload: { + messageType: { + case: "assistant", + value: { content: "Hello", reasoning: "", modelSlug: "gpt-4" }, + }, + }, + } as any, + }); + + // Simulate connection error + await stateMachine.handleEvent({ + type: "CONNECTION_ERROR", + payload: new Error("Network disconnected"), + }); + + const state = useStreamingStateMachine.getState(); + expect(state.state).toBe("error"); + expect(state.streamingMessage.parts[0].status).toBe("stale"); + }); + + it("should handle incomplete indicator", async () => { + const stateMachine = useStreamingStateMachine.getState(); + + await stateMachine.handleEvent({ + type: "INCOMPLETE", + payload: { reason: "truncated" } as any, + }); + + const indicator = useStreamingStateMachine.getState().incompleteIndicator; + expect(indicator).not.toBeNull(); + }); + }); + + describe("State Transitions", () => { + it("should follow correct state flow: idle → receiving → finalizing → idle", async () => { + const stateMachine = useStreamingStateMachine.getState(); + + // Initial state + expect(stateMachine.state).toBe("idle"); + + // Add user message and init + useStreamingStateMachine.setState({ + streamingMessage: { + parts: [{ id: "user-1", role: "user", status: "streaming", data: { content: "Hi" } }], + sequence: 1, + }, + }); + + await stateMachine.handleEvent({ + type: "INIT", + payload: { conversationId: "conv-1", modelSlug: "gpt-4" } as any, + }); + expect(useStreamingStateMachine.getState().state).toBe("receiving"); + + // Add and complete assistant message + await stateMachine.handleEvent({ + type: "PART_BEGIN", + payload: { + messageId: "assistant-1", + payload: { + messageType: { + case: "assistant", + value: { content: "", reasoning: "", modelSlug: "gpt-4" }, + }, + }, + } as any, + }); + + await stateMachine.handleEvent({ + type: "PART_END", + payload: { + messageId: "assistant-1", + payload: { + messageType: { + case: "assistant", + value: { content: "Hello!", reasoning: "", modelSlug: "gpt-4" }, + }, + }, + } as any, + }); + + // Finalize + await stateMachine.handleEvent({ + type: "FINALIZE", + payload: { conversationId: "conv-1" } as any, + }); + + expect(useStreamingStateMachine.getState().state).toBe("idle"); + }); + + it("should handle error state transition", async () => { + await useStreamingStateMachine.getState().handleEvent({ + type: "ERROR", + payload: { errorMessage: "Fatal error" } as any, + }); + + expect(useStreamingStateMachine.getState().state).toBe("error"); + }); + }); + + describe("Sequence Number Management", () => { + it("should increment sequence on each update", async () => { + const stateMachine = useStreamingStateMachine.getState(); + const initialSequence = stateMachine.streamingMessage.sequence; + + await stateMachine.handleEvent({ + type: "PART_BEGIN", + payload: { + messageId: "msg-1", + payload: { + messageType: { + case: "assistant", + value: { content: "", reasoning: "", modelSlug: "gpt-4" }, + }, + }, + } as any, + }); + + expect(useStreamingStateMachine.getState().streamingMessage.sequence).toBe( + initialSequence + 1 + ); + + await stateMachine.handleEvent({ + type: "CHUNK", + payload: { messageId: "msg-1", delta: "Hello" } as any, + }); + + expect(useStreamingStateMachine.getState().streamingMessage.sequence).toBe( + initialSequence + 2 + ); + }); + }); +}); diff --git a/webapp/_webapp/src/adapters/storage-adapter.ts b/webapp/_webapp/src/adapters/storage-adapter.ts index a4a2da6f..4b12e661 100644 --- a/webapp/_webapp/src/adapters/storage-adapter.ts +++ b/webapp/_webapp/src/adapters/storage-adapter.ts @@ -16,6 +16,7 @@ export class LocalStorageAdapter implements StorageAdapter { try { return localStorage.getItem(key); } catch { + // eslint-disable-next-line no-console console.warn("[Storage] localStorage.getItem failed for key:", key); return null; } @@ -25,6 +26,7 @@ export class LocalStorageAdapter implements StorageAdapter { try { localStorage.setItem(key, value); } catch (e) { + // eslint-disable-next-line no-console console.warn("[Storage] localStorage.setItem failed for key:", key, e); } } @@ -33,6 +35,7 @@ export class LocalStorageAdapter implements StorageAdapter { try { localStorage.removeItem(key); } catch (e) { + // eslint-disable-next-line no-console console.warn("[Storage] localStorage.removeItem failed for key:", key, e); } } @@ -41,6 +44,7 @@ export class LocalStorageAdapter implements StorageAdapter { try { localStorage.clear(); } catch (e) { + // eslint-disable-next-line no-console console.warn("[Storage] localStorage.clear failed", e); } } @@ -96,6 +100,7 @@ export function createStorageAdapter(type?: "localStorage" | "memory"): StorageA localStorage.removeItem(testKey); return new LocalStorageAdapter(); } catch { + // eslint-disable-next-line no-console console.warn("[Storage] localStorage not available, falling back to memory storage"); return new MemoryStorageAdapter(); } diff --git a/webapp/_webapp/src/components/branch-switcher.tsx b/webapp/_webapp/src/components/branch-switcher.tsx new file mode 100644 index 00000000..4c962e9f --- /dev/null +++ b/webapp/_webapp/src/components/branch-switcher.tsx @@ -0,0 +1,94 @@ +import { Icon } from "@iconify/react"; +import { Conversation } from "../pkg/gen/apiclient/chat/v2/chat_pb"; +import { getConversation } from "../query/api"; +import { useConversationStore } from "../stores/conversation/conversation-store"; +import { useStreamingStateMachine } from "../stores/streaming"; +import { useState } from "react"; + +interface BranchSwitcherProps { + conversation?: Conversation; +} + +export const BranchSwitcher = ({ conversation }: BranchSwitcherProps) => { + const [isLoading, setIsLoading] = useState(false); + const setCurrentConversation = useConversationStore((s) => s.setCurrentConversation); + const isStreaming = useConversationStore((s) => s.isStreaming); + + // Don't show if no branches or only one branch + const totalBranches = conversation?.totalBranches ?? 0; + if (totalBranches <= 1) { + return null; + } + + const currentIndex = conversation?.currentBranchIndex ?? 1; + const branches = conversation?.branches ?? []; + + const switchToBranch = async (branchId: string) => { + if (!conversation?.id || isLoading || isStreaming) return; + + setIsLoading(true); + try { + const response = await getConversation({ + conversationId: conversation.id, + branchId: branchId, + }); + if (response.conversation) { + setCurrentConversation(response.conversation); + useStreamingStateMachine.getState().reset(); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error("Failed to switch branch:", error); + } finally { + setIsLoading(false); + } + }; + + const handlePrevious = () => { + if (currentIndex <= 1) return; + const prevBranchId = branches[currentIndex - 2]?.id; + if (prevBranchId) { + switchToBranch(prevBranchId); + } + }; + + const handleNext = () => { + if (currentIndex >= totalBranches) return; + const nextBranchId = branches[currentIndex]?.id; + if (nextBranchId) { + switchToBranch(nextBranchId); + } + }; + + const canGoPrev = currentIndex > 1 && !isLoading && !isStreaming; + const canGoNext = currentIndex < totalBranches && !isLoading && !isStreaming; + + return ( +
+ + + + {isLoading ? "..." : `${currentIndex} / ${totalBranches}`} + + +
+ ); +}; diff --git a/webapp/_webapp/src/components/markdown.tsx b/webapp/_webapp/src/components/markdown.tsx index 3da2beba..9a675f54 100644 --- a/webapp/_webapp/src/components/markdown.tsx +++ b/webapp/_webapp/src/components/markdown.tsx @@ -1,6 +1,9 @@ -import Markdown from "markdown-to-jsx"; -import { TextPatches } from "./text-patches"; -import { ReactNode, useMemo, memo } from "react"; +import { memo } from "react"; +import { Streamdown } from "streamdown"; +import { code } from "@streamdown/code"; +import { mermaid } from "@streamdown/mermaid"; +import { math } from "@streamdown/math"; +import { cjk } from "@streamdown/cjk"; interface MarkdownComponentProps { children: string; @@ -8,129 +11,35 @@ interface MarkdownComponentProps { animated?: boolean; } -interface ComponentProps { - children: ReactNode; - [key: string]: ReactNode | string | number | boolean | undefined; -} - -const AnimatedText = ({ children, animated }: { children: ReactNode; animated?: boolean }) => { - if (!animated) { - return children; - } - - const str = typeof children === "string" ? children : children?.toString() || ""; - - if (str.length > 0 && !str.includes("[object Object]")) { - return str.split(" ").map((word, index) => ( - - {word + " "} - - )); - } - return children; -}; - +// @ts-ignore const MarkdownComponent = memo(({ children, prevAttachment, animated }: MarkdownComponentProps) => { - const markdownOptions = useMemo( - () => ({ - overrides: { - PaperDebugger: { - component: TextPatches, - }, - span: { - component: ({ children, ...props }: ComponentProps) => ( - - {children} - - ), - }, - // p: { - // component: ({ children, ...props }: ComponentProps) => ( - //
- // {children} - //
- // ), - // }, - h1: { - component: ({ children, ...props }: ComponentProps) => ( -

- {typeof children === "string" ? {children} : children} -

- ), - }, - h2: { - component: ({ children, ...props }: ComponentProps) => ( -

- {typeof children === "string" ? {children} : children} -

- ), - }, - h3: { - component: ({ children, ...props }: ComponentProps) => ( -

- {typeof children === "string" ? {children} : children} -

- ), - }, - code: { - component: ({ children, ...props }: ComponentProps) => ( - - {typeof children === "string" ? {children} : children} - - ), - }, - pre: { - component: ({ children, ...props }: ComponentProps) => ( - - {typeof children === "string" ? {children} : children} - - ), - }, - a: { - component: ({ children, ...props }: ComponentProps) => ( - - {typeof children === "string" ? {children} : children} - - ), - }, - hr: { - component: ({ ...props }: ComponentProps) =>
, - }, - li: { - component: ({ children, ...props }: ComponentProps) => ( -
  • - {typeof children === "string" ? {children} : children} -
  • - ), - }, - ul: { - component: ({ children, ...props }: ComponentProps) => ( -
      - {typeof children === "string" ? {children} : children} -
    - ), - }, - ol: { - component: ({ children, ...props }: ComponentProps) => ( -
      - {typeof children === "string" ? {children} : children} -
    - ), - }, - }, - }), - [prevAttachment, animated], - ); - - return {children}; + return ( +

    + {children} +

    + ), + + h2: ({ children }) => ( +

    + {children} +

    + ), + + h3: ({ children }) => ( +

    + {children} +

    + ), + }} + plugins={{ code, mermaid, math, cjk }} + isAnimating={animated}> + {children} +
    + + // return {children}; }); MarkdownComponent.displayName = "MarkdownComponent"; diff --git a/webapp/_webapp/src/components/message-card.tsx b/webapp/_webapp/src/components/message-card.tsx index 2a5f406c..36763eaa 100644 --- a/webapp/_webapp/src/components/message-card.tsx +++ b/webapp/_webapp/src/components/message-card.tsx @@ -1,7 +1,7 @@ import { cn } from "@heroui/react"; import { memo } from "react"; import Tools from "./message-entry-container/tools/tools"; -import { MessageEntry, MessageEntryStatus } from "../stores/conversation/types"; +import { DisplayMessage } from "../stores/types"; import { AssistantMessageContainer } from "./message-entry-container/assistant"; import { UserMessageContainer } from "./message-entry-container/user"; import { ToolCallPrepareMessageContainer } from "./message-entry-container/toolcall-prepare"; @@ -34,63 +34,77 @@ export const STYLES = { // Types interface MessageCardProps { - messageEntry: MessageEntry; + /** The display message to render (unified format) */ + message: DisplayMessage; + /** Selected text from the previous user message */ prevAttachment?: string; + /** ID of the previous message (for branching) */ + previousMessageId?: string; + /** Whether to animate the message (for streaming) */ animated?: boolean; } -export const MessageCard = memo(({ messageEntry, prevAttachment, animated }: MessageCardProps) => { +/** + * MessageCard component renders a single message in the chat. + * Now accepts DisplayMessage directly instead of MessageEntry. + */ +export const MessageCard = memo(({ message, prevAttachment, previousMessageId, animated }: MessageCardProps) => { + const isStale = message.status === "stale"; + const isPreparing = message.status === "streaming"; + const returnComponent = () => { - if (messageEntry.toolCall !== undefined) { + if (message.type === "toolCall") { return (
    ); } - if (messageEntry.assistant !== undefined) { + if (message.type === "assistant") { return ( ); } - if (messageEntry.toolCallPrepareArguments !== undefined) { + if (message.type === "toolCallPrepare") { return ( ); } - if (messageEntry.user !== undefined) { + if (message.type === "user") { return ( ); } - return ; + return ; }; return <>{returnComponent()}; diff --git a/webapp/_webapp/src/components/message-entry-container/assistant.tsx b/webapp/_webapp/src/components/message-entry-container/assistant.tsx index 80986082..bb6700b5 100644 --- a/webapp/_webapp/src/components/message-entry-container/assistant.tsx +++ b/webapp/_webapp/src/components/message-entry-container/assistant.tsx @@ -1,5 +1,6 @@ import { cn, Tooltip } from "@heroui/react"; -import { useCallback, useMemo, useState } from "react"; +import { GeneralToolCard } from "./tools/general"; +import { useCallback, useEffect, useMemo, useState } from "react"; import googleAnalytics from "../../libs/google-analytics"; import { getProjectId } from "../../libs/helpers"; import MarkdownComponent from "../markdown"; @@ -17,6 +18,7 @@ const preprocessMessage = (message: string): string | undefined => { export const AssistantMessageContainer = ({ message, + reasoning, messageId, animated, prevAttachment, @@ -24,6 +26,7 @@ export const AssistantMessageContainer = ({ preparing, }: { message: string; + reasoning?: string; messageId: string; animated: boolean; prevAttachment: string; @@ -35,6 +38,24 @@ export const AssistantMessageContainer = ({ const projectId = getProjectId(); const [copySuccess, setCopySuccess] = useState(false); + // Auto-collapse reasoning when message content arrives + const [isReasoningCollapsed, setIsReasoningCollapsed] = useState(true); + + useEffect(() => { + const hasReasoning = (reasoning?.length ?? 0) > 0; + const hasMessage = (processedMessage?.length ?? 0) > 0; + + // Auto-expand when reasoning arrives + if (hasReasoning && !hasMessage) { + setIsReasoningCollapsed(false); + } + + // Auto-collapse when message content arrives + if (hasReasoning && hasMessage) { + setIsReasoningCollapsed(true); + } + }, [reasoning, processedMessage]); + const handleCopy = useCallback(() => { if (processedMessage) { googleAnalytics.fireEvent(user?.id, "messagecard_copy_message", { @@ -49,7 +70,7 @@ export const AssistantMessageContainer = ({ } }, [user?.id, projectId, processedMessage, messageId]); - const showMessage = processedMessage?.length || 0 > 0; + const showMessage = (processedMessage?.length ?? 0) > 0 || (reasoning?.length ?? 0) > 0; const staleComponent = stale &&
    This message is stale.
    ; const writingIndicator = stale || !showMessage ? null : ( @@ -64,10 +85,25 @@ export const AssistantMessageContainer = ({ )} /> ); + + const reasoningComponent = reasoning && ( + setIsReasoningCollapsed(!isReasoningCollapsed)} + isLoading={preparing} + /> + + ); return ( showMessage && (
    + {/* Reasoning content */} + {reasoningComponent} + {/* Message content */}
    @@ -80,15 +116,17 @@ export const AssistantMessageContainer = ({ {/* Stale message */} {staleComponent} + { (processedMessage?.length || 0) > 0 &&
    -
    +
    }
    ) ); }; + diff --git a/webapp/_webapp/src/components/message-entry-container/attachment-popover.tsx b/webapp/_webapp/src/components/message-entry-container/attachment-popover.tsx index 73b042ee..2d38b5cf 100644 --- a/webapp/_webapp/src/components/message-entry-container/attachment-popover.tsx +++ b/webapp/_webapp/src/components/message-entry-container/attachment-popover.tsx @@ -5,7 +5,7 @@ import { STYLES } from "../message-card"; export const AttachmentPopover = ({ attachment }: { attachment: string }) => ( - attachment + attachment
    diff --git a/webapp/_webapp/src/components/message-entry-container/tools/error.tsx b/webapp/_webapp/src/components/message-entry-container/tools/error.tsx index c90da685..4ac5044a 100644 --- a/webapp/_webapp/src/components/message-entry-container/tools/error.tsx +++ b/webapp/_webapp/src/components/message-entry-container/tools/error.tsx @@ -1,4 +1,4 @@ -import { cn } from "@heroui/react"; +import { GeneralToolCard } from "./general"; type ErrorToolCardProps = { functionName: string; @@ -7,12 +7,17 @@ type ErrorToolCardProps = { }; export const ErrorToolCard = ({ functionName, errorMessage, animated }: ErrorToolCardProps) => { - return ( -
    -

    - Error in Tool "{functionName}" -

    - {errorMessage} -
    - ); + // return ( + //
    + //

    + // Error in Tool "{functionName}" + //

    + // {errorMessage} + //
    ) + return ; }; diff --git a/webapp/_webapp/src/components/message-entry-container/tools/general.tsx b/webapp/_webapp/src/components/message-entry-container/tools/general.tsx index f521acda..3d1b104f 100644 --- a/webapp/_webapp/src/components/message-entry-container/tools/general.tsx +++ b/webapp/_webapp/src/components/message-entry-container/tools/general.tsx @@ -1,10 +1,18 @@ import { cn } from "@heroui/react"; -import { useState } from "react"; +import { useEffect, useState, useRef } from "react"; +import { Streamdown } from "streamdown"; +import { code } from "@streamdown/code"; +import { mermaid } from "@streamdown/mermaid"; +import { math } from "@streamdown/math"; +import { cjk } from "@streamdown/cjk"; type GeneralToolCardProps = { functionName: string; message: string; animated: boolean; + isCollapsed?: boolean; + onToggleCollapse?: () => void; + isLoading?: boolean; }; const shimmerStyle = { @@ -20,8 +28,40 @@ const shimmerStyle = { backgroundPositionX: "-100%", } as const; -export const GeneralToolCard = ({ functionName, message, animated }: GeneralToolCardProps) => { - const [isCollapsed, setIsCollapsed] = useState(true); +export const GeneralToolCard = ({ functionName, message, animated, isCollapsed: externalIsCollapsed, onToggleCollapse, isLoading }: GeneralToolCardProps) => { + const [internalIsCollapsed, setInternalIsCollapsed] = useState(true); + const scrollContainerRef = useRef(null); + + // Use external state if provided, otherwise use internal state + const isCollapsed = externalIsCollapsed !== undefined ? externalIsCollapsed : internalIsCollapsed; + + // Sync internal state with external state when it changes + useEffect(() => { + if (externalIsCollapsed !== undefined) { + setInternalIsCollapsed(externalIsCollapsed); + } + }, [externalIsCollapsed]); + + // Auto-scroll to bottom when message updates and is loading + useEffect(() => { + if (isLoading && scrollContainerRef.current && !isCollapsed) { + scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight; + } + }, [message, isLoading, isCollapsed]); + + // Auto-collapse when loading finishes (reasoning ends) + const prevIsLoadingRef = useRef(isLoading); + useEffect(() => { + // Only collapse if it was loading before and now it's not + if (prevIsLoadingRef.current && !isLoading) { + if (onToggleCollapse && externalIsCollapsed === false) { + onToggleCollapse(); + } else if (externalIsCollapsed === undefined) { + setInternalIsCollapsed(true); + } + } + prevIsLoadingRef.current = isLoading; + }, [isLoading, onToggleCollapse, externalIsCollapsed]); // When no message, show minimal "Calling tool..." style like Preparing function if (!message) { @@ -35,10 +75,14 @@ export const GeneralToolCard = ({ functionName, message, animated }: GeneralTool } const toggleCollapse = () => { - setIsCollapsed(!isCollapsed); + if (onToggleCollapse) { + onToggleCollapse(); + } else { + setInternalIsCollapsed(!internalIsCollapsed); + } }; const pascalCase = (str: string) => { - const words = str.split("_"); + const words = str.split(/[\s_-]+/); return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" "); }; // When there is a message, show the compact card with collapsible content @@ -46,7 +90,7 @@ export const GeneralToolCard = ({ functionName, message, animated }: GeneralTool
    -

    {pascalCase(functionName)}

    +

    {pascalCase(functionName)}

    +
    - {message} +
    +
    + {/* Fade-out gradient at top */} +
    + + {/* Scrollable content with max height - hide scrollbar */} +
    + + {message} + +
    + + {/* Fade-out gradient at bottom */} +
    +
    +
    ); diff --git a/webapp/_webapp/src/components/message-entry-container/tools/xtramcp/online-search-papers.tsx b/webapp/_webapp/src/components/message-entry-container/tools/xtramcp/online-search-papers.tsx index f0b0da72..9ef7cb96 100644 --- a/webapp/_webapp/src/components/message-entry-container/tools/xtramcp/online-search-papers.tsx +++ b/webapp/_webapp/src/components/message-entry-container/tools/xtramcp/online-search-papers.tsx @@ -84,7 +84,7 @@ export const OnlineSearchPapersCard = ({ functionName, message, preparing, anima {/* Metadata dropdown - INSIDE the tool card */} {result.metadata && Object.keys(result.metadata).length > 0 && ( -
    +
    {/* Custom metadata rendering */} {result.metadata.query && (
    diff --git a/webapp/_webapp/src/components/message-entry-container/tools/xtramcp/review-paper.tsx b/webapp/_webapp/src/components/message-entry-container/tools/xtramcp/review-paper.tsx index 799928a1..d6ac1f9f 100644 --- a/webapp/_webapp/src/components/message-entry-container/tools/xtramcp/review-paper.tsx +++ b/webapp/_webapp/src/components/message-entry-container/tools/xtramcp/review-paper.tsx @@ -111,7 +111,7 @@ export const ReviewPaperCard = ({ functionName, message, preparing, animated }: {/* Metadata dropdown - INSIDE the tool card */} {result.metadata && Object.keys(result.metadata).length > 0 && ( -
    +
    {/* Informational note */}
    diff --git a/webapp/_webapp/src/components/message-entry-container/tools/xtramcp/utils/common.tsx b/webapp/_webapp/src/components/message-entry-container/tools/xtramcp/utils/common.tsx index 15b66877..a4fd668b 100644 --- a/webapp/_webapp/src/components/message-entry-container/tools/xtramcp/utils/common.tsx +++ b/webapp/_webapp/src/components/message-entry-container/tools/xtramcp/utils/common.tsx @@ -69,7 +69,7 @@ interface CollapseArrowButtonProps { export const CollapseArrowButton = ({ isCollapsed, ariaLabel }: CollapseArrowButtonProps) => (