diff --git a/api/chat_main_handler.go b/api/chat_main_handler.go index 79f690df..9ed20385 100644 --- a/api/chat_main_handler.go +++ b/api/chat_main_handler.go @@ -85,7 +85,6 @@ type BotRequest struct { type ChatInstructionResponse struct { ArtifactInstruction string `json:"artifactInstruction"` - ToolInstruction string `json:"toolInstruction"` } func (h *ChatHandler) GetChatInstructions(w http.ResponseWriter, r *http.Request) { @@ -95,15 +94,8 @@ func (h *ChatHandler) GetChatInstructions(w http.ResponseWriter, r *http.Request artifactInstruction = "" } - toolInstruction, err := loadToolInstruction() - if err != nil { - log.Printf("Warning: Failed to load tool instruction: %v", err) - toolInstruction = "" - } - json.NewEncoder(w).Encode(ChatInstructionResponse{ ArtifactInstruction: artifactInstruction, - ToolInstruction: toolInstruction, }) } diff --git a/api/chat_main_service.go b/api/chat_main_service.go index 2aaf58a7..9b3d6bf5 100644 --- a/api/chat_main_service.go +++ b/api/chat_main_service.go @@ -27,9 +27,6 @@ type ChatService struct { //go:embed artifact_instruction.txt var artifactInstructionText string -//go:embed tool_instruction.txt -var toolInstructionText string - // NewChatService creates a new ChatService with database queries. func NewChatService(q *sqlc_queries.Queries) *ChatService { return &ChatService{q: q} @@ -44,15 +41,6 @@ func loadArtifactInstruction() (string, error) { return artifactInstructionText, nil } -// loadToolInstruction loads the tool-use instruction from file. -// Returns the instruction content or an error if the file cannot be read. -func loadToolInstruction() (string, error) { - if toolInstructionText == "" { - return "", eris.New("tool instruction text is empty") - } - return toolInstructionText, nil -} - func appendInstructionToSystemMessage(msgs []models.Message, instruction string) { if instruction == "" || len(msgs) == 0 { return @@ -139,16 +127,6 @@ func (s *ChatService) getAskMessages(chatSession sqlc_queries.ChatSession, chatU appendInstructionToSystemMessage(msgs, artifactInstruction) } - if chatSession.CodeRunnerEnabled { - toolInstruction, err := loadToolInstruction() - if err != nil { - log.Printf("Warning: Failed to load tool instruction: %v", err) - toolInstruction = "" - } - - appendInstructionToSystemMessage(msgs, toolInstruction) - } - return msgs, nil } diff --git a/api/chat_session_handler.go b/api/chat_session_handler.go index 6b475ee9..898dc02c 100644 --- a/api/chat_session_handler.go +++ b/api/chat_session_handler.go @@ -54,13 +54,12 @@ func (h *ChatSessionHandler) getChatSessionByUUID(w http.ResponseWriter, r *http } session_resp := &ChatSessionResponse{ - Uuid: session.Uuid, - Topic: session.Topic, - MaxLength: session.MaxLength, - CreatedAt: session.CreatedAt, - UpdatedAt: session.UpdatedAt, - CodeRunnerEnabled: session.CodeRunnerEnabled, - ArtifactEnabled: session.ArtifactEnabled, + Uuid: session.Uuid, + Topic: session.Topic, + MaxLength: session.MaxLength, + CreatedAt: session.CreatedAt, + UpdatedAt: session.UpdatedAt, + ArtifactEnabled: session.ArtifactEnabled, } json.NewEncoder(w).Encode(session_resp) } @@ -146,20 +145,19 @@ func (h *ChatSessionHandler) createChatSessionByUUID(w http.ResponseWriter, r *h } type UpdateChatSessionRequest struct { - Uuid string `json:"uuid"` - Topic string `json:"topic"` - MaxLength int32 `json:"maxLength"` - Temperature float64 `json:"temperature"` - Model string `json:"model"` - TopP float64 `json:"topP"` - N int32 `json:"n"` - MaxTokens int32 `json:"maxTokens"` - Debug bool `json:"debug"` - SummarizeMode bool `json:"summarizeMode"` - CodeRunnerEnabled bool `json:"codeRunnerEnabled"` - ArtifactEnabled bool `json:"artifactEnabled"` - ExploreMode bool `json:"exploreMode"` - WorkspaceUUID string `json:"workspaceUuid,omitempty"` + Uuid string `json:"uuid"` + Topic string `json:"topic"` + MaxLength int32 `json:"maxLength"` + Temperature float64 `json:"temperature"` + Model string `json:"model"` + TopP float64 `json:"topP"` + N int32 `json:"n"` + MaxTokens int32 `json:"maxTokens"` + Debug bool `json:"debug"` + SummarizeMode bool `json:"summarizeMode"` + ArtifactEnabled bool `json:"artifactEnabled"` + ExploreMode bool `json:"exploreMode"` + WorkspaceUUID string `json:"workspaceUuid,omitempty"` } // UpdateChatSessionByUUID updates a chat session by its UUID @@ -197,7 +195,6 @@ func (h *ChatSessionHandler) createOrUpdateChatSessionByUUID(w http.ResponseWrit sessionParams.MaxTokens = sessionReq.MaxTokens sessionParams.Debug = sessionReq.Debug sessionParams.SummarizeMode = sessionReq.SummarizeMode - sessionParams.CodeRunnerEnabled = sessionReq.CodeRunnerEnabled sessionParams.ArtifactEnabled = sessionReq.ArtifactEnabled sessionParams.ExploreMode = sessionReq.ExploreMode @@ -377,20 +374,19 @@ func (h *ChatSessionHandler) createChatSessionFromSnapshot(w http.ResponseWriter sessionUUID := uuid.New().String() session, err := h.service.q.CreateOrUpdateChatSessionByUUID(r.Context(), sqlc_queries.CreateOrUpdateChatSessionByUUIDParams{ - Uuid: sessionUUID, - UserID: userID, - Topic: sessionTitle, - MaxLength: originSession.MaxLength, - Temperature: originSession.Temperature, - Model: originSession.Model, - MaxTokens: originSession.MaxTokens, - TopP: originSession.TopP, - Debug: originSession.Debug, - SummarizeMode: originSession.SummarizeMode, - CodeRunnerEnabled: originSession.CodeRunnerEnabled, - ExploreMode: originSession.ExploreMode, - WorkspaceID: originSession.WorkspaceID, - N: 1, + Uuid: sessionUUID, + UserID: userID, + Topic: sessionTitle, + MaxLength: originSession.MaxLength, + Temperature: originSession.Temperature, + Model: originSession.Model, + MaxTokens: originSession.MaxTokens, + TopP: originSession.TopP, + Debug: originSession.Debug, + SummarizeMode: originSession.SummarizeMode, + ExploreMode: originSession.ExploreMode, + WorkspaceID: originSession.WorkspaceID, + N: 1, }) if err != nil { apiErr := ErrInternalUnexpected diff --git a/api/chat_session_service.go b/api/chat_session_service.go index 80eb876f..05a4be18 100644 --- a/api/chat_session_service.go +++ b/api/chat_session_service.go @@ -87,20 +87,19 @@ func (s *ChatSessionService) GetSimpleChatSessionsByUserID(ctx context.Context, } return SimpleChatSession{ - Uuid: session.Uuid, - IsEdit: false, - Title: session.Topic, - MaxLength: int(session.MaxLength), - Temperature: float64(session.Temperature), - TopP: float64(session.TopP), - N: session.N, - MaxTokens: session.MaxTokens, - Debug: session.Debug, - Model: session.Model, - SummarizeMode: session.SummarizeMode, - CodeRunnerEnabled: session.CodeRunnerEnabled, - ArtifactEnabled: session.ArtifactEnabled, - WorkspaceUuid: workspaceUuid, + Uuid: session.Uuid, + IsEdit: false, + Title: session.Topic, + MaxLength: int(session.MaxLength), + Temperature: float64(session.Temperature), + TopP: float64(session.TopP), + N: session.N, + MaxTokens: session.MaxTokens, + Debug: session.Debug, + Model: session.Model, + SummarizeMode: session.SummarizeMode, + ArtifactEnabled: session.ArtifactEnabled, + WorkspaceUuid: workspaceUuid, } }) return simple_sessions, nil diff --git a/api/chat_workspace_handler.go b/api/chat_workspace_handler.go index 1e3f1730..e035abb2 100644 --- a/api/chat_workspace_handler.go +++ b/api/chat_workspace_handler.go @@ -626,7 +626,6 @@ func (h *ChatWorkspaceHandler) createSessionInWorkspace(w http.ResponseWriter, r "uuid": session.Uuid, "topic": session.Topic, "model": session.Model, - "codeRunnerEnabled": session.CodeRunnerEnabled, "artifactEnabled": session.ArtifactEnabled, "workspaceUuid": workspaceUUID, "createdAt": session.CreatedAt.Format("2006-01-02T15:04:05Z"), @@ -693,7 +692,6 @@ func (h *ChatWorkspaceHandler) getSessionsByWorkspace(w http.ResponseWriter, r * "debug": session.Debug, "summarizeMode": session.SummarizeMode, "exploreMode": session.ExploreMode, - "codeRunnerEnabled": session.CodeRunnerEnabled, "artifactEnabled": session.ArtifactEnabled, "createdAt": session.CreatedAt.Format("2006-01-02T15:04:05Z"), "updatedAt": session.UpdatedAt.Format("2006-01-02T15:04:05Z"), diff --git a/api/embed_debug_test.go b/api/embed_debug_test.go index cf9e69c7..bf81cf2a 100644 --- a/api/embed_debug_test.go +++ b/api/embed_debug_test.go @@ -6,7 +6,4 @@ func TestEmbedInstructions(t *testing.T) { if artifactInstructionText == "" { t.Fatalf("artifactInstructionText is empty") } - if toolInstructionText == "" { - t.Fatalf("toolInstructionText is empty") - } } diff --git a/api/models.go b/api/models.go index bceae31b..15b375fc 100644 --- a/api/models.go +++ b/api/models.go @@ -56,20 +56,19 @@ func (msg SimpleChatMessage) GetRole() string { } type SimpleChatSession struct { - Uuid string `json:"uuid"` - IsEdit bool `json:"isEdit"` - Title string `json:"title"` - MaxLength int `json:"maxLength"` - Temperature float64 `json:"temperature"` - TopP float64 `json:"topP"` - N int32 `json:"n"` - MaxTokens int32 `json:"maxTokens"` - Debug bool `json:"debug"` - Model string `json:"model"` - SummarizeMode bool `json:"summarizeMode"` - CodeRunnerEnabled bool `json:"codeRunnerEnabled"` - ArtifactEnabled bool `json:"artifactEnabled"` - WorkspaceUuid string `json:"workspaceUuid"` + Uuid string `json:"uuid"` + IsEdit bool `json:"isEdit"` + Title string `json:"title"` + MaxLength int `json:"maxLength"` + Temperature float64 `json:"temperature"` + TopP float64 `json:"topP"` + N int32 `json:"n"` + MaxTokens int32 `json:"maxTokens"` + Debug bool `json:"debug"` + Model string `json:"model"` + SummarizeMode bool `json:"summarizeMode"` + ArtifactEnabled bool `json:"artifactEnabled"` + WorkspaceUuid string `json:"workspaceUuid"` } type ChatMessageResponse struct { @@ -87,13 +86,12 @@ type ChatMessageResponse struct { } type ChatSessionResponse struct { - Uuid string `json:"uuid"` - Topic string `json:"topic"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - MaxLength int32 `json:"maxLength"` - CodeRunnerEnabled bool `json:"codeRunnerEnabled"` - ArtifactEnabled bool `json:"artifactEnabled"` + Uuid string `json:"uuid"` + Topic string `json:"topic"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + MaxLength int32 `json:"maxLength"` + ArtifactEnabled bool `json:"artifactEnabled"` } type Pagination struct { diff --git a/api/sqlc/queries/chat_session.sql b/api/sqlc/queries/chat_session.sql index eee0b513..7c1e415f 100644 --- a/api/sqlc/queries/chat_session.sql +++ b/api/sqlc/queries/chat_session.sql @@ -41,8 +41,8 @@ WHERE uuid = $1 RETURNING *; -- name: CreateOrUpdateChatSessionByUUID :one -INSERT INTO chat_session(uuid, user_id, topic, max_length, temperature, model, max_tokens, top_p, n, debug, summarize_mode, code_runner_enabled, workspace_id, explore_mode, artifact_enabled) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) +INSERT INTO chat_session(uuid, user_id, topic, max_length, temperature, model, max_tokens, top_p, n, debug, summarize_mode, workspace_id, explore_mode, artifact_enabled) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) ON CONFLICT (uuid) DO UPDATE SET max_length = EXCLUDED.max_length, @@ -53,7 +53,6 @@ top_p = EXCLUDED.top_p, n= EXCLUDED.n, model = EXCLUDED.model, summarize_mode = EXCLUDED.summarize_mode, -code_runner_enabled = EXCLUDED.code_runner_enabled, artifact_enabled = EXCLUDED.artifact_enabled, workspace_id = CASE WHEN EXCLUDED.workspace_id IS NOT NULL THEN EXCLUDED.workspace_id ELSE chat_session.workspace_id END, topic = CASE WHEN chat_session.topic IS NULL THEN EXCLUDED.topic ELSE chat_session.topic END, diff --git a/api/sqlc/schema.sql b/api/sqlc/schema.sql index 958d2ca3..94a061f7 100644 --- a/api/sqlc/schema.sql +++ b/api/sqlc/schema.sql @@ -170,13 +170,13 @@ CREATE TABLE IF NOT EXISTS chat_session ( max_tokens int DEFAULT 4096 NOT NULL, n integer DEFAULT 1 NOT NULL, summarize_mode boolean DEFAULT false NOT NULL, - code_runner_enabled boolean DEFAULT false NOT NULL, workspace_id INTEGER REFERENCES chat_workspace(id) ON DELETE SET NULL, artifact_enabled boolean DEFAULT false NOT NULL ); -- chat_session +ALTER TABLE chat_session DROP COLUMN IF EXISTS code_runner_enabled; ALTER TABLE chat_session ADD COLUMN IF NOT EXISTS temperature float DEFAULT 1.0 NOT NULL; ALTER TABLE chat_session ADD COLUMN IF NOT EXISTS top_p float DEFAULT 1.0 NOT NULL; ALTER TABLE chat_session ADD COLUMN IF NOT EXISTS max_tokens int DEFAULT 4096 NOT NULL; @@ -185,7 +185,6 @@ ALTER TABLE chat_session ADD COLUMN IF NOT EXISTS explore_mode boolean DEFAULT f ALTER TABlE chat_session ADD COLUMN IF NOT EXISTS model character varying(255) NOT NULL DEFAULT 'gpt-3.5-turbo'; ALTER TABLE chat_session ADD COLUMN IF NOT EXISTS n INTEGER DEFAULT 1 NOT NULL; ALTER TABLE chat_session ADD COLUMN IF NOT EXISTS summarize_mode boolean DEFAULT false NOT NULL; -ALTER TABLE chat_session ADD COLUMN IF NOT EXISTS code_runner_enabled boolean DEFAULT false NOT NULL; ALTER TABLE chat_session ADD COLUMN IF NOT EXISTS workspace_id INTEGER REFERENCES chat_workspace(id) ON DELETE SET NULL; ALTER TABLE chat_session ADD COLUMN IF NOT EXISTS artifact_enabled boolean DEFAULT false NOT NULL; diff --git a/api/sqlc_queries/chat_session.sql.go b/api/sqlc_queries/chat_session.sql.go index 7e8e2eb9..a2a1e8dd 100644 --- a/api/sqlc_queries/chat_session.sql.go +++ b/api/sqlc_queries/chat_session.sql.go @@ -14,7 +14,7 @@ import ( const createChatSession = `-- name: CreateChatSession :one INSERT INTO chat_session (user_id, topic, max_length, uuid, model) VALUES ($1, $2, $3, $4, $5) -RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, code_runner_enabled, workspace_id, artifact_enabled, debug, explore_mode +RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode ` type CreateChatSessionParams struct { @@ -49,7 +49,6 @@ func (q *Queries) CreateChatSession(ctx context.Context, arg CreateChatSessionPa &i.MaxTokens, &i.N, &i.SummarizeMode, - &i.CodeRunnerEnabled, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, @@ -61,7 +60,7 @@ func (q *Queries) CreateChatSession(ctx context.Context, arg CreateChatSessionPa const createChatSessionByUUID = `-- name: CreateChatSessionByUUID :one INSERT INTO chat_session (user_id, uuid, topic, created_at, active, max_length, model) VALUES ($1, $2, $3, $4, $5, $6, $7) -RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, code_runner_enabled, workspace_id, artifact_enabled, debug, explore_mode +RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode ` type CreateChatSessionByUUIDParams struct { @@ -100,7 +99,6 @@ func (q *Queries) CreateChatSessionByUUID(ctx context.Context, arg CreateChatSes &i.MaxTokens, &i.N, &i.SummarizeMode, - &i.CodeRunnerEnabled, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, @@ -112,7 +110,7 @@ func (q *Queries) CreateChatSessionByUUID(ctx context.Context, arg CreateChatSes const createChatSessionInWorkspace = `-- name: CreateChatSessionInWorkspace :one INSERT INTO chat_session (user_id, uuid, topic, created_at, active, max_length, model, workspace_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) -RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, code_runner_enabled, workspace_id, artifact_enabled, debug, explore_mode +RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode ` type CreateChatSessionInWorkspaceParams struct { @@ -153,7 +151,6 @@ func (q *Queries) CreateChatSessionInWorkspace(ctx context.Context, arg CreateCh &i.MaxTokens, &i.N, &i.SummarizeMode, - &i.CodeRunnerEnabled, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, @@ -163,8 +160,8 @@ func (q *Queries) CreateChatSessionInWorkspace(ctx context.Context, arg CreateCh } const createOrUpdateChatSessionByUUID = `-- name: CreateOrUpdateChatSessionByUUID :one -INSERT INTO chat_session(uuid, user_id, topic, max_length, temperature, model, max_tokens, top_p, n, debug, summarize_mode, code_runner_enabled, workspace_id, explore_mode, artifact_enabled) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) +INSERT INTO chat_session(uuid, user_id, topic, max_length, temperature, model, max_tokens, top_p, n, debug, summarize_mode, workspace_id, explore_mode, artifact_enabled) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) ON CONFLICT (uuid) DO UPDATE SET max_length = EXCLUDED.max_length, @@ -175,13 +172,12 @@ top_p = EXCLUDED.top_p, n= EXCLUDED.n, model = EXCLUDED.model, summarize_mode = EXCLUDED.summarize_mode, -code_runner_enabled = EXCLUDED.code_runner_enabled, artifact_enabled = EXCLUDED.artifact_enabled, workspace_id = CASE WHEN EXCLUDED.workspace_id IS NOT NULL THEN EXCLUDED.workspace_id ELSE chat_session.workspace_id END, topic = CASE WHEN chat_session.topic IS NULL THEN EXCLUDED.topic ELSE chat_session.topic END, explore_mode = EXCLUDED.explore_mode, updated_at = now() -returning id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, code_runner_enabled, workspace_id, artifact_enabled, debug, explore_mode +returning id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode ` type CreateOrUpdateChatSessionByUUIDParams struct { @@ -196,7 +192,6 @@ type CreateOrUpdateChatSessionByUUIDParams struct { N int32 `json:"n"` Debug bool `json:"debug"` SummarizeMode bool `json:"summarizeMode"` - CodeRunnerEnabled bool `json:"codeRunnerEnabled"` WorkspaceID sql.NullInt32 `json:"workspaceId"` ExploreMode bool `json:"exploreMode"` ArtifactEnabled bool `json:"artifactEnabled"` @@ -215,7 +210,6 @@ func (q *Queries) CreateOrUpdateChatSessionByUUID(ctx context.Context, arg Creat arg.N, arg.Debug, arg.SummarizeMode, - arg.CodeRunnerEnabled, arg.WorkspaceID, arg.ExploreMode, arg.ArtifactEnabled, @@ -236,7 +230,6 @@ func (q *Queries) CreateOrUpdateChatSessionByUUID(ctx context.Context, arg Creat &i.MaxTokens, &i.N, &i.SummarizeMode, - &i.CodeRunnerEnabled, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, @@ -258,7 +251,7 @@ func (q *Queries) DeleteChatSession(ctx context.Context, id int32) error { const deleteChatSessionByUUID = `-- name: DeleteChatSessionByUUID :exec update chat_session set active = false WHERE uuid = $1 -returning id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, code_runner_enabled, workspace_id, artifact_enabled, debug, explore_mode +returning id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode ` func (q *Queries) DeleteChatSessionByUUID(ctx context.Context, uuid string) error { @@ -267,7 +260,7 @@ func (q *Queries) DeleteChatSessionByUUID(ctx context.Context, uuid string) erro } const getAllChatSessions = `-- name: GetAllChatSessions :many -SELECT id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, code_runner_enabled, workspace_id, artifact_enabled, debug, explore_mode FROM chat_session +SELECT id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode FROM chat_session where active = true ORDER BY id ` @@ -296,7 +289,6 @@ func (q *Queries) GetAllChatSessions(ctx context.Context) ([]ChatSession, error) &i.MaxTokens, &i.N, &i.SummarizeMode, - &i.CodeRunnerEnabled, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, @@ -316,7 +308,7 @@ func (q *Queries) GetAllChatSessions(ctx context.Context) ([]ChatSession, error) } const getChatSessionByID = `-- name: GetChatSessionByID :one -SELECT id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, code_runner_enabled, workspace_id, artifact_enabled, debug, explore_mode FROM chat_session WHERE id = $1 +SELECT id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode FROM chat_session WHERE id = $1 ` func (q *Queries) GetChatSessionByID(ctx context.Context, id int32) (ChatSession, error) { @@ -337,7 +329,6 @@ func (q *Queries) GetChatSessionByID(ctx context.Context, id int32) (ChatSession &i.MaxTokens, &i.N, &i.SummarizeMode, - &i.CodeRunnerEnabled, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, @@ -347,7 +338,7 @@ func (q *Queries) GetChatSessionByID(ctx context.Context, id int32) (ChatSession } const getChatSessionByUUID = `-- name: GetChatSessionByUUID :one -SELECT id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, code_runner_enabled, workspace_id, artifact_enabled, debug, explore_mode FROM chat_session +SELECT id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode FROM chat_session WHERE active = true and uuid = $1 order by updated_at ` @@ -370,7 +361,6 @@ func (q *Queries) GetChatSessionByUUID(ctx context.Context, uuid string) (ChatSe &i.MaxTokens, &i.N, &i.SummarizeMode, - &i.CodeRunnerEnabled, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, @@ -380,7 +370,7 @@ func (q *Queries) GetChatSessionByUUID(ctx context.Context, uuid string) (ChatSe } const getChatSessionByUUIDWithInActive = `-- name: GetChatSessionByUUIDWithInActive :one -SELECT id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, code_runner_enabled, workspace_id, artifact_enabled, debug, explore_mode FROM chat_session +SELECT id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode FROM chat_session WHERE uuid = $1 order by updated_at ` @@ -403,7 +393,6 @@ func (q *Queries) GetChatSessionByUUIDWithInActive(ctx context.Context, uuid str &i.MaxTokens, &i.N, &i.SummarizeMode, - &i.CodeRunnerEnabled, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, @@ -413,7 +402,7 @@ func (q *Queries) GetChatSessionByUUIDWithInActive(ctx context.Context, uuid str } const getChatSessionsByUserID = `-- name: GetChatSessionsByUserID :many -SELECT cs.id, cs.user_id, cs.uuid, cs.topic, cs.created_at, cs.updated_at, cs.active, cs.model, cs.max_length, cs.temperature, cs.top_p, cs.max_tokens, cs.n, cs.summarize_mode, cs.code_runner_enabled, cs.workspace_id, cs.artifact_enabled, cs.debug, cs.explore_mode +SELECT cs.id, cs.user_id, cs.uuid, cs.topic, cs.created_at, cs.updated_at, cs.active, cs.model, cs.max_length, cs.temperature, cs.top_p, cs.max_tokens, cs.n, cs.summarize_mode, cs.workspace_id, cs.artifact_enabled, cs.debug, cs.explore_mode FROM chat_session cs LEFT JOIN ( SELECT chat_session_uuid, MAX(created_at) AS latest_message_time @@ -450,7 +439,6 @@ func (q *Queries) GetChatSessionsByUserID(ctx context.Context, userID int32) ([] &i.MaxTokens, &i.N, &i.SummarizeMode, - &i.CodeRunnerEnabled, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, @@ -470,7 +458,7 @@ func (q *Queries) GetChatSessionsByUserID(ctx context.Context, userID int32) ([] } const getSessionsByWorkspaceID = `-- name: GetSessionsByWorkspaceID :many -SELECT cs.id, cs.user_id, cs.uuid, cs.topic, cs.created_at, cs.updated_at, cs.active, cs.model, cs.max_length, cs.temperature, cs.top_p, cs.max_tokens, cs.n, cs.summarize_mode, cs.code_runner_enabled, cs.workspace_id, cs.artifact_enabled, cs.debug, cs.explore_mode +SELECT cs.id, cs.user_id, cs.uuid, cs.topic, cs.created_at, cs.updated_at, cs.active, cs.model, cs.max_length, cs.temperature, cs.top_p, cs.max_tokens, cs.n, cs.summarize_mode, cs.workspace_id, cs.artifact_enabled, cs.debug, cs.explore_mode FROM chat_session cs LEFT JOIN ( SELECT chat_session_uuid, MAX(created_at) AS latest_message_time @@ -507,7 +495,6 @@ func (q *Queries) GetSessionsByWorkspaceID(ctx context.Context, workspaceID sql. &i.MaxTokens, &i.N, &i.SummarizeMode, - &i.CodeRunnerEnabled, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, @@ -528,7 +515,7 @@ func (q *Queries) GetSessionsByWorkspaceID(ctx context.Context, workspaceID sql. const getSessionsGroupedByWorkspace = `-- name: GetSessionsGroupedByWorkspace :many SELECT - cs.id, cs.user_id, cs.uuid, cs.topic, cs.created_at, cs.updated_at, cs.active, cs.model, cs.max_length, cs.temperature, cs.top_p, cs.max_tokens, cs.n, cs.summarize_mode, cs.code_runner_enabled, cs.workspace_id, cs.artifact_enabled, cs.debug, cs.explore_mode, + cs.id, cs.user_id, cs.uuid, cs.topic, cs.created_at, cs.updated_at, cs.active, cs.model, cs.max_length, cs.temperature, cs.top_p, cs.max_tokens, cs.n, cs.summarize_mode, cs.workspace_id, cs.artifact_enabled, cs.debug, cs.explore_mode, w.uuid as workspace_uuid, w.name as workspace_name, w.color as workspace_color, @@ -562,7 +549,6 @@ type GetSessionsGroupedByWorkspaceRow struct { MaxTokens int32 `json:"maxTokens"` N int32 `json:"n"` SummarizeMode bool `json:"summarizeMode"` - CodeRunnerEnabled bool `json:"codeRunnerEnabled"` WorkspaceID sql.NullInt32 `json:"workspaceId"` ArtifactEnabled bool `json:"artifactEnabled"` Debug bool `json:"debug"` @@ -597,7 +583,6 @@ func (q *Queries) GetSessionsGroupedByWorkspace(ctx context.Context, userID int3 &i.MaxTokens, &i.N, &i.SummarizeMode, - &i.CodeRunnerEnabled, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, @@ -621,7 +606,7 @@ func (q *Queries) GetSessionsGroupedByWorkspace(ctx context.Context, userID int3 } const getSessionsWithoutWorkspace = `-- name: GetSessionsWithoutWorkspace :many -SELECT id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, code_runner_enabled, workspace_id, artifact_enabled, debug, explore_mode FROM chat_session +SELECT id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode FROM chat_session WHERE user_id = $1 AND workspace_id IS NULL AND active = true ` @@ -649,7 +634,6 @@ func (q *Queries) GetSessionsWithoutWorkspace(ctx context.Context, userID int32) &i.MaxTokens, &i.N, &i.SummarizeMode, - &i.CodeRunnerEnabled, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, @@ -711,7 +695,7 @@ func (q *Queries) MigrateSessionsToDefaultWorkspace(ctx context.Context, arg Mig const updateChatSession = `-- name: UpdateChatSession :one UPDATE chat_session SET user_id = $2, topic = $3, updated_at = now(), active = $4 WHERE id = $1 -RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, code_runner_enabled, workspace_id, artifact_enabled, debug, explore_mode +RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode ` type UpdateChatSessionParams struct { @@ -744,7 +728,6 @@ func (q *Queries) UpdateChatSession(ctx context.Context, arg UpdateChatSessionPa &i.MaxTokens, &i.N, &i.SummarizeMode, - &i.CodeRunnerEnabled, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, @@ -756,7 +739,7 @@ func (q *Queries) UpdateChatSession(ctx context.Context, arg UpdateChatSessionPa const updateChatSessionByUUID = `-- name: UpdateChatSessionByUUID :one UPDATE chat_session SET user_id = $2, topic = $3, updated_at = now() WHERE uuid = $1 -RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, code_runner_enabled, workspace_id, artifact_enabled, debug, explore_mode +RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode ` type UpdateChatSessionByUUIDParams struct { @@ -783,7 +766,6 @@ func (q *Queries) UpdateChatSessionByUUID(ctx context.Context, arg UpdateChatSes &i.MaxTokens, &i.N, &i.SummarizeMode, - &i.CodeRunnerEnabled, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, @@ -799,7 +781,7 @@ ON CONFLICT (uuid) DO UPDATE SET topic = EXCLUDED.topic, updated_at = now() -returning id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, code_runner_enabled, workspace_id, artifact_enabled, debug, explore_mode +returning id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode ` type UpdateChatSessionTopicByUUIDParams struct { @@ -826,7 +808,6 @@ func (q *Queries) UpdateChatSessionTopicByUUID(ctx context.Context, arg UpdateCh &i.MaxTokens, &i.N, &i.SummarizeMode, - &i.CodeRunnerEnabled, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, @@ -840,7 +821,7 @@ UPDATE chat_session SET max_length = $2, updated_at = now() WHERE uuid = $1 -RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, code_runner_enabled, workspace_id, artifact_enabled, debug, explore_mode +RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode ` type UpdateSessionMaxLengthParams struct { @@ -866,7 +847,6 @@ func (q *Queries) UpdateSessionMaxLength(ctx context.Context, arg UpdateSessionM &i.MaxTokens, &i.N, &i.SummarizeMode, - &i.CodeRunnerEnabled, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, @@ -879,7 +859,7 @@ const updateSessionWorkspace = `-- name: UpdateSessionWorkspace :one UPDATE chat_session SET workspace_id = $2, updated_at = now() WHERE uuid = $1 -RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, code_runner_enabled, workspace_id, artifact_enabled, debug, explore_mode +RETURNING id, user_id, uuid, topic, created_at, updated_at, active, model, max_length, temperature, top_p, max_tokens, n, summarize_mode, workspace_id, artifact_enabled, debug, explore_mode ` type UpdateSessionWorkspaceParams struct { @@ -905,7 +885,6 @@ func (q *Queries) UpdateSessionWorkspace(ctx context.Context, arg UpdateSessionW &i.MaxTokens, &i.N, &i.SummarizeMode, - &i.CodeRunnerEnabled, &i.WorkspaceID, &i.ArtifactEnabled, &i.Debug, diff --git a/api/sqlc_queries/models.go b/api/sqlc_queries/models.go index cf9cb24b..c64219fa 100644 --- a/api/sqlc_queries/models.go +++ b/api/sqlc_queries/models.go @@ -146,7 +146,6 @@ type ChatSession struct { MaxTokens int32 `json:"maxTokens"` N int32 `json:"n"` SummarizeMode bool `json:"summarizeMode"` - CodeRunnerEnabled bool `json:"codeRunnerEnabled"` WorkspaceID sql.NullInt32 `json:"workspaceId"` ArtifactEnabled bool `json:"artifactEnabled"` Debug bool `json:"debug"` diff --git a/api/tool_instruction.txt b/api/tool_instruction.txt deleted file mode 100644 index 1966f069..00000000 --- a/api/tool_instruction.txt +++ /dev/null @@ -1,51 +0,0 @@ -TOOL USE (CODE RUNNER ENABLED): - -When you need to execute code or inspect file contents, use the tool call format below. -Only use tools that are listed as available. Do not invent tools. - -Available tools: -- run_code: Execute JavaScript/TypeScript or Python in a sandboxed runner. -- read_vfs: Read a file from the Virtual File System. -- write_vfs: Write a file to the Virtual File System. -- list_vfs: List directory entries in the Virtual File System. -- stat_vfs: Get metadata for a file or directory in the Virtual File System. - -Tool call format (exact): -```tool_call -{"name":"run_code","arguments":{"language":"python","code":"print('hello')"}} -``` - -```tool_call -{"name":"read_vfs","arguments":{"path":"/data/iris.csv","encoding":"utf8"}} -``` - -```tool_call -{"name":"read_vfs","arguments":{"path":"/data/image.png","encoding":"binary"}} -``` - -```tool_call -{"name":"write_vfs","arguments":{"path":"/data/output.txt","content":"hello","encoding":"utf8"}} -``` - -```tool_call -{"name":"write_vfs","arguments":{"path":"/data/blob.bin","content":"","encoding":"base64"}} -``` - -```tool_call -{"name":"list_vfs","arguments":{"path":"/data"}} -``` - -```tool_call -{"name":"stat_vfs","arguments":{"path":"/data/iris.csv"}} -``` - -Rules: -- When you emit a tool call, do NOT include extra explanation in the same response. -- Wait for a tool result message before continuing. -- Tool results are returned in this format: -```tool_result -{"name":"run_code","success":true,"results":[{"type":"stdout","content":"hello"}]} -``` -- Tool result messages may include a leading line: [[TOOL_RESULT]]. Ignore that line. -- After receiving a tool result, continue with a normal response (and artifacts if needed). -- Use VFS paths like /data or /workspace when reading files in code. diff --git a/web/public/workers/jsRunner.js b/web/public/workers/jsRunner.js deleted file mode 100644 index b67d4aaa..00000000 --- a/web/public/workers/jsRunner.js +++ /dev/null @@ -1,990 +0,0 @@ -/** - * JavaScript Code Runner Web Worker - * Executes JavaScript code in a safe, isolated environment with library support - */ - -// Simplified VFS implementation for JavaScript Runner -class SimpleVFS { - constructor() { - this.files = new Map() - this.directories = new Set(['/']) - this.metadata = new Map() // Add metadata support - this.currentDirectory = '/workspace' - - // Create default directories - this.directories.add('/data') - this.directories.add('/tmp') - this.directories.add('/workspace') - } - - writeFile(path, data, options = {}) { - const normalizedPath = this.normalize(path) - this.ensureDirectoryExists(this.dirname(normalizedPath)) - this.files.set(normalizedPath, data) - return normalizedPath - } - - readFile(path, encoding = 'utf8') { - const normalizedPath = this.normalize(path) - if (!this.files.has(normalizedPath)) { - throw new Error(`File not found: ${path}`) - } - return this.files.get(normalizedPath) - } - - exists(path) { - const normalizedPath = this.normalize(path) - return this.files.has(normalizedPath) || this.directories.has(normalizedPath) - } - - mkdir(path, options = {}) { - const normalizedPath = this.normalize(path) - if (options.recursive) { - const parts = normalizedPath.split('/').filter(Boolean) - let currentPath = '/' - for (const part of parts) { - currentPath = currentPath === '/' ? '/' + part : currentPath + '/' + part - this.directories.add(currentPath) - } - } else { - this.directories.add(normalizedPath) - } - return normalizedPath - } - - readdir(path) { - const normalizedPath = this.normalize(path) - if (!this.directories.has(normalizedPath)) { - throw new Error(`Directory not found: ${path}`) - } - - const items = [] - const prefix = normalizedPath === '/' ? '/' : normalizedPath + '/' - - // Find immediate children - for (const dir of this.directories) { - if (dir !== normalizedPath && dir.startsWith(prefix)) { - const relative = dir.slice(prefix.length) - if (!relative.includes('/')) { - items.push(relative) - } - } - } - - for (const file of this.files.keys()) { - if (file.startsWith(prefix)) { - const relative = file.slice(prefix.length) - if (!relative.includes('/')) { - items.push(relative) - } - } - } - - return items.sort() - } - - stat(path) { - const normalizedPath = this.normalize(path) - if (this.directories.has(normalizedPath)) { - return { isDirectory: true, isFile: false } - } - if (this.files.has(normalizedPath)) { - return { isDirectory: false, isFile: true } - } - throw new Error(`Path not found: ${path}`) - } - - unlink(path) { - const normalizedPath = this.normalize(path) - if (!this.files.has(normalizedPath)) { - throw new Error(`File not found: ${path}`) - } - this.files.delete(normalizedPath) - } - - rmdir(path) { - const normalizedPath = this.normalize(path) - if (!this.directories.has(normalizedPath)) { - throw new Error(`Directory not found: ${path}`) - } - this.directories.delete(normalizedPath) - } - - chdir(path) { - const normalizedPath = this.normalize(path) - if (!this.directories.has(normalizedPath)) { - throw new Error(`Directory not found: ${path}`) - } - this.currentDirectory = normalizedPath - } - - getcwd() { - return this.currentDirectory - } - - normalize(path) { - if (!path || path === '.') return this.currentDirectory - if (!path.startsWith('/')) { - path = this.currentDirectory + '/' + path - } - - const parts = path.split('/').filter(Boolean) - const resolved = [] - - for (const part of parts) { - if (part === '.') continue - if (part === '..') { - resolved.pop() - } else { - resolved.push(part) - } - } - - return '/' + resolved.join('/') - } - - dirname(path) { - const normalized = this.normalize(path) - if (normalized === '/') return '/' - const lastSlash = normalized.lastIndexOf('/') - return lastSlash === 0 ? '/' : normalized.slice(0, lastSlash) - } - - ensureDirectoryExists(path) { - if (!this.directories.has(path)) { - this.mkdir(path, { recursive: true }) - } - } -} - -// Global VFS instance -let vfs = null - -class SafeJSRunner { - constructor() { - this.output = [] - this.loadedLibraries = new Set() - this.libraryCache = new Map() - this.executionStats = { - startTime: 0, - memoryUsage: 0, - operationCount: 0, - maxOperations: 100000, // Prevent infinite loops - maxMemory: 50 * 1024 * 1024 // 50MB limit (approximate) - } - this.setupConsole() - this.setupVFS() - this.setupFS() - } - - setupVFS() { - // Initialize Virtual File System - if (!vfs) { - vfs = new SimpleVFS() - this.addOutput('info', 'Virtual file system initialized') - } - } - - setupFS() { - // Create Node.js-style fs module - this.fs = { - // Synchronous versions - readFileSync: (path, options = {}) => { - const encoding = options.encoding || options - return vfs.readFile(path, encoding) - }, - - writeFileSync: (path, data, options = {}) => { - return vfs.writeFile(path, data, options) - }, - - existsSync: (path) => { - return vfs.exists(path) - }, - - mkdirSync: (path, options = {}) => { - return vfs.mkdir(path, options) - }, - - readdirSync: (path, options = {}) => { - return vfs.readdir(path) - }, - - statSync: (path) => { - return vfs.stat(path) - }, - - unlinkSync: (path) => { - return vfs.unlink(path) - }, - - rmdirSync: (path, options = {}) => { - return vfs.rmdir(path) - }, - - // Async versions (Promise-based) - readFile: async (path, options = {}) => { - const encoding = options.encoding || options - return vfs.readFile(path, encoding) - }, - - writeFile: async (path, data, options = {}) => { - return vfs.writeFile(path, data, options) - }, - - mkdir: async (path, options = {}) => { - return vfs.mkdir(path, options) - }, - - readdir: async (path, options = {}) => { - return vfs.readdir(path) - }, - - stat: async (path) => { - return vfs.stat(path) - }, - - unlink: async (path) => { - return vfs.unlink(path) - }, - - rmdir: async (path, options = {}) => { - return vfs.rmdir(path) - }, - - // Additional utilities - promises: { - readFile: async (path, options = {}) => { - const encoding = options.encoding || options - return vfs.readFile(path, encoding) - }, - - writeFile: async (path, data, options = {}) => { - return vfs.writeFile(path, data, options) - }, - - mkdir: async (path, options = {}) => { - return vfs.mkdir(path, options) - }, - - readdir: async (path, options = {}) => { - return vfs.readdir(path) - }, - - stat: async (path) => { - return vfs.stat(path) - }, - - unlink: async (path) => { - return vfs.unlink(path) - }, - - rmdir: async (path, options = {}) => { - return vfs.rmdir(path) - } - } - } - - // Path utilities - this.path = { - join: (...parts) => { - const joined = parts.join('/') - return vfs.normalize(joined) - }, - - dirname: (path) => { - return vfs.dirname(path) - }, - - basename: (path) => { - const normalized = vfs.normalize(path) - const lastSlash = normalized.lastIndexOf('/') - return normalized.slice(lastSlash + 1) - }, - - extname: (path) => { - const base = this.path.basename(path) - const lastDot = base.lastIndexOf('.') - if (lastDot === -1 || lastDot === 0) return '' - return base.slice(lastDot) - }, - - resolve: (...paths) => { - return vfs.normalize(paths.join('/')) - }, - - relative: (from, to) => { - // Simple relative path implementation - const fromParts = vfs.normalize(from).split('/').filter(Boolean) - const toParts = vfs.normalize(to).split('/').filter(Boolean) - - let commonLength = 0 - for (let i = 0; i < Math.min(fromParts.length, toParts.length); i++) { - if (fromParts[i] === toParts[i]) { - commonLength++ - } else { - break - } - } - - const upLevels = fromParts.length - commonLength - const relativeParts = Array(upLevels).fill('..').concat(toParts.slice(commonLength)) - - return relativeParts.join('/') || '.' - }, - - isAbsolute: (path) => { - return path.startsWith('/') - } - } - } - - setupConsole() { - // Capture console methods and redirect output - this.console = { - log: (...args) => this.addOutput('log', this.formatArgs(args)), - error: (...args) => this.addOutput('error', this.formatArgs(args)), - warn: (...args) => this.addOutput('warn', this.formatArgs(args)), - info: (...args) => this.addOutput('info', this.formatArgs(args)), - debug: (...args) => this.addOutput('debug', this.formatArgs(args)), - clear: () => { - this.output = [] - this.addOutput('info', 'Console cleared') - } - } - } - - formatArgs(args) { - return args.map(arg => { - if (typeof arg === 'object' && arg !== null) { - try { - return JSON.stringify(arg, null, 2) - } catch (e) { - return String(arg) - } - } - return String(arg) - }).join(' ') - } - - addOutput(type, content) { - this.output.push({ - id: Date.now().toString() + Math.random().toString(36).substr(2, 9), - type: type, - content: String(content), - timestamp: new Date().toISOString() - }) - } - - createSafeTimeout(fn, ms) { - // Limit timeout duration to prevent infinite delays - const maxMs = Math.min(ms, 5000) // Max 5 seconds - return setTimeout(fn, maxMs) - } - - createSafeInterval(fn, ms) { - // Limit interval frequency to prevent overwhelming execution - const minMs = Math.max(ms, 100) // Min 100ms - return setInterval(fn, minMs) - } - - // Available libraries with their CDN URLs - getAvailableLibraries() { - return { - 'lodash': 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js', - 'd3': 'https://d3js.org/d3.v7.min.js', - 'chart.js': 'https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.min.js', - 'moment': 'https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js', - 'axios': 'https://cdn.jsdelivr.net/npm/axios@1.6.0/dist/axios.min.js', - 'rxjs': 'https://cdn.jsdelivr.net/npm/rxjs@7.8.1/dist/bundles/rxjs.umd.min.js', - 'p5': 'https://cdn.jsdelivr.net/npm/p5@1.7.0/lib/p5.min.js', - 'three': 'https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.min.js', - 'fabric': 'https://cdn.jsdelivr.net/npm/fabric@5.3.0/dist/fabric.min.js' - } - } - - // Load a library from CDN - async loadLibrary(libraryName) { - if (this.loadedLibraries.has(libraryName)) { - return true // Already loaded - } - - const libraries = this.getAvailableLibraries() - const url = libraries[libraryName.toLowerCase()] - - if (!url) { - throw new Error(`Library '${libraryName}' is not available. Available libraries: ${Object.keys(libraries).join(', ')}`) - } - - try { - // Check cache first - if (this.libraryCache.has(url)) { - const cachedCode = this.libraryCache.get(url) - eval(cachedCode) - this.loadedLibraries.add(libraryName) - this.addOutput('info', `Library '${libraryName}' loaded from cache`) - return true - } - - // Fetch library code - const response = await fetch(url) - if (!response.ok) { - throw new Error(`Failed to load library: ${response.status} ${response.statusText}`) - } - - const libraryCode = await response.text() - - // Cache the library code - this.libraryCache.set(url, libraryCode) - - // Execute library code in global scope - eval(libraryCode) - - this.loadedLibraries.add(libraryName) - this.addOutput('info', `Library '${libraryName}' loaded successfully`) - return true - } catch (error) { - this.addOutput('error', `Failed to load library '${libraryName}': ${error.message}`) - return false - } - } - - // Parse code for library import statements - parseLibraryImports(code) { - const importRegex = /\/\/\s*@import\s+(\w+)/gi - const imports = [] - let match - - while ((match = importRegex.exec(code)) !== null) { - imports.push(match[1]) - } - - return imports - } - - // Create a virtual canvas for graphics operations - createVirtualCanvas(width = 400, height = 300) { - const canvas = { - width: width, - height: height, - data: [], - - getContext: (type) => { - if (type === '2d') { - return { - // Basic 2D context methods - fillStyle: '#000000', - strokeStyle: '#000000', - lineWidth: 1, - - fillRect: function(x, y, w, h) { - canvas.data.push({ - type: 'fillRect', - x, y, width: w, height: h, - style: this.fillStyle - }) - }, - - strokeRect: function(x, y, w, h) { - canvas.data.push({ - type: 'strokeRect', - x, y, width: w, height: h, - style: this.strokeStyle, - lineWidth: this.lineWidth - }) - }, - - beginPath: () => canvas.data.push({ type: 'beginPath' }), - closePath: () => canvas.data.push({ type: 'closePath' }), - - moveTo: (x, y) => canvas.data.push({ type: 'moveTo', x, y }), - lineTo: (x, y) => canvas.data.push({ type: 'lineTo', x, y }), - - arc: (x, y, radius, startAngle, endAngle) => { - canvas.data.push({ type: 'arc', x, y, radius, startAngle, endAngle }) - }, - - fill: function() { canvas.data.push({ type: 'fill', style: this.fillStyle }) }, - stroke: function() { canvas.data.push({ type: 'stroke', style: this.strokeStyle, lineWidth: this.lineWidth }) }, - - clearRect: (x, y, w, h) => canvas.data.push({ type: 'clearRect', x, y, width: w, height: h }), - - // Text methods - fillText: function(text, x, y) { canvas.data.push({ type: 'fillText', text, x, y, style: this.fillStyle }) }, - strokeText: function(text, x, y) { canvas.data.push({ type: 'strokeText', text, x, y, style: this.strokeStyle }) } - } - } - return null - }, - - toDataURL: () => { - // Return canvas operations as JSON for later rendering - return JSON.stringify({ - type: 'canvas', - width: canvas.width, - height: canvas.height, - operations: canvas.data - }) - } - } - - return canvas - } - - // Memory and performance monitoring - checkResourceLimits() { - // Increment operation count - this.executionStats.operationCount++ - - // Check operation limit - if (this.executionStats.operationCount > this.executionStats.maxOperations) { - throw new Error(`Operation limit exceeded: ${this.executionStats.maxOperations} operations`) - } - - // Estimate memory usage (rough approximation) - const memoryEstimate = this.output.length * 1000 + - this.loadedLibraries.size * 100000 + - this.libraryCache.size * 200000 - - if (memoryEstimate > this.executionStats.maxMemory) { - throw new Error(`Memory limit exceeded: ~${Math.round(memoryEstimate / 1024 / 1024)}MB`) - } - - this.executionStats.memoryUsage = memoryEstimate - } - - // Create monitored timeout with resource checking - createMonitoredTimeout(fn, ms) { - const maxMs = Math.min(ms, 5000) - return setTimeout(() => { - try { - this.checkResourceLimits() - fn() - } catch (error) { - this.addOutput('error', `Timeout callback error: ${error.message}`) - } - }, maxMs) - } - - // Create monitored interval with resource checking - createMonitoredInterval(fn, ms) { - const minMs = Math.max(ms, 100) - const intervalId = setInterval(() => { - try { - this.checkResourceLimits() - fn() - } catch (error) { - this.addOutput('error', `Interval callback error: ${error.message}`) - clearInterval(intervalId) - } - }, minMs) - return intervalId - } - - // Enhanced execution environment with monitoring - createSecureFunction(code) { - // Direct execution without nested eval to preserve console context - return code - } - - async execute(code) { - this.output = [] - this.executionStats.startTime = performance.now() - this.executionStats.operationCount = 0 - this.executionStats.memoryUsage = 0 - - try { - // Parse and load any required libraries - const requiredLibraries = this.parseLibraryImports(code) - for (const library of requiredLibraries) { - await this.loadLibrary(library) - } - - // Create safe execution context with enhanced globals - const safeGlobals = { - // Console methods - console: this.console, - - // Safe built-in objects - Math: Math, - Date: Date, - Array: Array, - Object: Object, - String: String, - Number: Number, - Boolean: Boolean, - JSON: JSON, - RegExp: RegExp, - - // Enhanced timer functions with monitoring - setTimeout: this.createMonitoredTimeout.bind(this), - setInterval: this.createMonitoredInterval.bind(this), - clearTimeout: clearTimeout, - clearInterval: clearInterval, - - // Performance monitoring - performance: { now: performance.now.bind(performance) }, - - // Safe Promise support - Promise: Promise, - - // Error handling - Error: Error, - TypeError: TypeError, - ReferenceError: ReferenceError, - SyntaxError: SyntaxError, - - // Utility functions - isNaN: isNaN, - isFinite: isFinite, - parseInt: parseInt, - parseFloat: parseFloat, - - // Canvas and graphics support - createCanvas: this.createVirtualCanvas.bind(this), - - // Library management - loadLibrary: this.loadLibrary.bind(this), - getAvailableLibraries: this.getAvailableLibraries.bind(this), - - // File system support - fs: this.fs, - require: (module) => { - if (module === 'fs') return this.fs - if (module === 'path') return this.path - throw new Error(`Module '${module}' not found`) - }, - - // Process simulation - process: { - cwd: () => vfs.getcwd(), - chdir: (path) => vfs.chdir(path), - env: {}, - argv: ['node'], - version: 'v18.0.0', - platform: 'browser' - }, - - // Resource monitoring (internal use) - checkResourceLimits: this.checkResourceLimits.bind(this), - - // Enhanced crypto support - crypto: { - getRandomValues: crypto.getRandomValues.bind(crypto), - randomUUID: crypto.randomUUID ? crypto.randomUUID.bind(crypto) : () => { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - const r = Math.random() * 16 | 0 - const v = c == 'x' ? r : (r & 0x3 | 0x8) - return v.toString(16) - }) - } - }, - - // Text encoding/decoding - TextEncoder: typeof TextEncoder !== 'undefined' ? TextEncoder : undefined, - TextDecoder: typeof TextDecoder !== 'undefined' ? TextDecoder : undefined, - - // Enhanced URL support - URL: typeof URL !== 'undefined' ? URL : undefined, - URLSearchParams: typeof URLSearchParams !== 'undefined' ? URLSearchParams : undefined, - - // Prevent access to dangerous globals - eval: undefined, - Function: undefined, - window: undefined, - global: undefined, - self: undefined, - document: undefined, - XMLHttpRequest: undefined, - fetch: undefined, - WebSocket: undefined, - Worker: undefined, - SharedWorker: undefined, - ServiceWorker: undefined, - localStorage: undefined, - sessionStorage: undefined, - indexedDB: undefined, - location: undefined, - navigator: undefined, - history: undefined - } - - // Execute code in safe context with enhanced monitoring - const secureCode = this.createSecureFunction(code) - - // Set up periodic resource checking - const checkInterval = setInterval(() => { - try { - this.checkResourceLimits() - } catch (error) { - clearInterval(checkInterval) - throw error - } - }, 100) - - let result - try { - result = new Function( - ...Object.keys(safeGlobals), - ` - try { - return (function() { - ${secureCode} - })(); - } catch (error) { - console.error('Runtime Error: ' + error.message); - throw error; - } - ` - )(...Object.values(safeGlobals)) - } finally { - clearInterval(checkInterval) - } - - // Add return value if it exists and is not undefined - if (result !== undefined) { - let formattedResult = result - - // Handle different return types - if (typeof result === 'object' && result !== null) { - // Check if it's a canvas operation result - if (result.toDataURL && typeof result.toDataURL === 'function') { - try { - const canvasData = result.toDataURL() - this.addOutput('canvas', canvasData) - return this.output - } catch (e) { - this.addOutput('error', `Canvas error: ${e.message}`) - } - } - - // Check if it's already a canvas data object - if (result.type === 'canvas' && result.operations) { - this.addOutput('canvas', JSON.stringify(result)) - return this.output - } - - // Handle other objects - try { - formattedResult = JSON.stringify(result, null, 2) - } catch (e) { - formattedResult = String(result) - } - } - - this.addOutput('return', formattedResult) - } - - // Add execution statistics - const executionTime = Math.round(performance.now() - this.executionStats.startTime) - const memoryUsed = Math.round(this.executionStats.memoryUsage / 1024 / 1024 * 100) / 100 - const operations = this.executionStats.operationCount - - this.addOutput('info', `Execution completed in ${executionTime}ms | ~${memoryUsed}MB | ${operations} ops`) - - return this.output - } catch (error) { - // Handle syntax and runtime errors - const executionTime = Math.round(performance.now() - this.executionStats.startTime) - const memoryUsed = Math.round(this.executionStats.memoryUsage / 1024 / 1024 * 100) / 100 - const operations = this.executionStats.operationCount - - this.addOutput('error', `${error.name}: ${error.message}`) - this.addOutput('info', `Execution failed after ${executionTime}ms | ~${memoryUsed}MB | ${operations} ops`) - return this.output - } - } -} - -// Create runner instance -const runner = new SafeJSRunner() - -// Handle messages from main thread -self.onmessage = async (e) => { - const { type, code, timeout = 10000, requestId, libraryName, path, data, options } = e.data - - try { - if (type === 'execute') { - // Set up execution timeout - let timeoutId - const timeoutPromise = new Promise((_, reject) => { - timeoutId = setTimeout(() => { - reject(new Error(`Code execution timed out after ${timeout}ms`)) - }, timeout) - }) - - // Execute code with timeout - const executionPromise = runner.execute(code) - - const results = await Promise.race([executionPromise, timeoutPromise]) - - // Clear timeout if execution completed in time - clearTimeout(timeoutId) - - // Send results back to main thread - self.postMessage({ - type: 'results', - data: results, - requestId: requestId - }) - } else if (type === 'loadLibrary') { - // Handle library loading requests - const success = await runner.loadLibrary(libraryName) - self.postMessage({ - type: 'libraryLoaded', - data: { success, libraryName }, - requestId: requestId - }) - } else if (type === 'getAvailableLibraries') { - // Handle library list requests - const libraries = runner.getAvailableLibraries() - self.postMessage({ - type: 'availableLibraries', - data: libraries, - requestId: requestId - }) - } else if (type === 'vfs') { - // Handle VFS operations - const { operation } = e.data - let result - - try { - switch (operation) { - case 'writeFile': - result = vfs.writeFile(path, data, options) - break - case 'readFile': - result = vfs.readFile(path, options?.encoding) - break - case 'exists': - result = vfs.exists(path) - break - case 'mkdir': - result = vfs.mkdir(path, options) - break - case 'readdir': - result = vfs.readdir(path) - break - case 'stat': - result = vfs.stat(path) - break - case 'unlink': - result = vfs.unlink(path) - break - case 'rmdir': - result = vfs.rmdir(path) - break - case 'chdir': - result = vfs.chdir(path) - break - case 'getcwd': - result = vfs.getcwd() - break - default: - throw new Error(`Unknown VFS operation: ${operation}`) - } - - self.postMessage({ - type: 'vfsResult', - data: result, - requestId: requestId - }) - } catch (error) { - self.postMessage({ - type: 'vfsError', - data: { message: error.message }, - requestId: requestId - }) - } - } else if (type === 'syncVFS') { - // Handle VFS synchronization from main thread - try { - const { vfsState } = e.data - if (vfsState && vfsState.files && vfsState.directories) { - // Clear current VFS state - vfs.files.clear() - vfs.directories.clear() - vfs.metadata.clear() - - // Import files from serialized state (array of [path, content] pairs) - if (Array.isArray(vfsState.files)) { - vfsState.files.forEach(([path, content]) => { - vfs.files.set(path, content) - }) - } - - // Import directories from serialized state (array of paths) - if (Array.isArray(vfsState.directories)) { - vfsState.directories.forEach(dir => { - vfs.directories.add(dir) - }) - } - - // Import metadata from serialized state (array of [path, meta] pairs) - if (Array.isArray(vfsState.metadata)) { - vfsState.metadata.forEach(([path, meta]) => { - // Convert ISO strings back to Date objects - const processedMeta = { - ...meta, - mtime: meta.mtime && typeof meta.mtime === 'string' ? new Date(meta.mtime) : meta.mtime, - created: meta.created && typeof meta.created === 'string' ? new Date(meta.created) : meta.created - } - vfs.metadata.set(path, processedMeta) - }) - } - - // Update current directory - if (vfsState.currentDirectory) { - vfs.currentDirectory = vfsState.currentDirectory - } - - runner.addOutput('info', `VFS synchronized: ${vfs.files.size} files, ${vfs.directories.size} directories`) - } - } catch (error) { - runner.addOutput('error', `VFS sync failed: ${error.message}`) - } - } else { - // Backward compatibility - treat as execute - const executionPromise = runner.execute(code) - const results = await executionPromise - - self.postMessage({ - type: 'results', - data: results, - requestId: requestId - }) - } - } catch (error) { - // Handle timeout or other errors - self.postMessage({ - type: 'error', - data: { - message: error.message, - name: error.name || 'ExecutionError' - }, - requestId: requestId - }) - } -} - -// Handle worker errors -self.onerror = (error) => { - self.postMessage({ - type: 'error', - data: { - message: error.message || 'Unknown worker error', - name: 'WorkerError' - } - }) -} - -// Handle unhandled promise rejections -self.onunhandledrejection = (event) => { - self.postMessage({ - type: 'error', - data: { - message: event.reason?.message || 'Unhandled promise rejection', - name: 'UnhandledRejection' - } - }) -} \ No newline at end of file diff --git a/web/public/workers/pyRunner.js b/web/public/workers/pyRunner.js deleted file mode 100644 index a47f4ef5..00000000 --- a/web/public/workers/pyRunner.js +++ /dev/null @@ -1,1579 +0,0 @@ -/** - * Python Code Runner Web Worker - * Executes Python code using Pyodide in a safe, isolated environment - */ - -// Simplified VFS implementation for Python Runner -class SimpleVFS { - constructor() { - this.files = new Map() - this.directories = new Set(['/']) - this.metadata = new Map() // Add metadata support - this.currentDirectory = '/workspace' - - // Create default directories - this.directories.add('/data') - this.directories.add('/tmp') - this.directories.add('/workspace') - } - - writeFile(path, data, options = {}) { - const normalizedPath = this.normalize(path) - this.ensureDirectoryExists(this.dirname(normalizedPath)) - const isBinary = options.binary || data instanceof ArrayBuffer || data instanceof Uint8Array - let storedData = data - if (isBinary) { - if (data instanceof ArrayBuffer) { - storedData = new Uint8Array(data) - } else if (data instanceof Uint8Array) { - storedData = data - } else if (typeof data === 'string') { - storedData = new TextEncoder().encode(data) - } - } else { - storedData = String(data) - } - this.files.set(normalizedPath, storedData) - return normalizedPath - } - - readFile(path, encoding = 'utf8') { - const normalizedPath = this.normalize(path) - if (!this.files.has(normalizedPath)) { - throw new Error(`File not found: ${path}`) - } - const data = this.files.get(normalizedPath) - if (encoding === 'binary' || encoding === null) { - if (typeof data === 'string') { - return new TextEncoder().encode(data) - } - return data - } - if (data instanceof Uint8Array) { - return new TextDecoder(encoding).decode(data) - } - return String(data) - } - - exists(path) { - const normalizedPath = this.normalize(path) - return this.files.has(normalizedPath) || this.directories.has(normalizedPath) - } - - mkdir(path, options = {}) { - const normalizedPath = this.normalize(path) - if (options.recursive) { - const parts = normalizedPath.split('/').filter(Boolean) - let currentPath = '/' - for (const part of parts) { - currentPath = currentPath === '/' ? '/' + part : currentPath + '/' + part - this.directories.add(currentPath) - } - } else { - this.directories.add(normalizedPath) - } - return normalizedPath - } - - readdir(path) { - const normalizedPath = this.normalize(path) - if (!this.directories.has(normalizedPath)) { - throw new Error(`Directory not found: ${path}`) - } - - const items = [] - const prefix = normalizedPath === '/' ? '/' : normalizedPath + '/' - - // Find immediate children - for (const dir of this.directories) { - if (dir !== normalizedPath && dir.startsWith(prefix)) { - const relative = dir.slice(prefix.length) - if (!relative.includes('/')) { - items.push(relative) - } - } - } - - for (const file of this.files.keys()) { - if (file.startsWith(prefix)) { - const relative = file.slice(prefix.length) - if (!relative.includes('/')) { - items.push(relative) - } - } - } - - return items.sort() - } - - stat(path) { - const normalizedPath = this.normalize(path) - if (this.directories.has(normalizedPath)) { - return { isDirectory: true, isFile: false } - } - if (this.files.has(normalizedPath)) { - return { isDirectory: false, isFile: true } - } - throw new Error(`Path not found: ${path}`) - } - - unlink(path) { - const normalizedPath = this.normalize(path) - if (!this.files.has(normalizedPath)) { - throw new Error(`File not found: ${path}`) - } - this.files.delete(normalizedPath) - } - - rmdir(path) { - const normalizedPath = this.normalize(path) - if (!this.directories.has(normalizedPath)) { - throw new Error(`Directory not found: ${path}`) - } - this.directories.delete(normalizedPath) - } - - chdir(path) { - const normalizedPath = this.normalize(path) - if (!this.directories.has(normalizedPath)) { - throw new Error(`Directory not found: ${path}`) - } - this.currentDirectory = normalizedPath - } - - getcwd() { - return this.currentDirectory - } - - normalize(path) { - if (!path || path === '.') return this.currentDirectory - if (!path.startsWith('/')) { - path = this.currentDirectory + '/' + path - } - - const parts = path.split('/').filter(Boolean) - const resolved = [] - - for (const part of parts) { - if (part === '.') continue - if (part === '..') { - resolved.pop() - } else { - resolved.push(part) - } - } - - return '/' + resolved.join('/') - } - - dirname(path) { - const normalized = this.normalize(path) - if (normalized === '/') return '/' - const lastSlash = normalized.lastIndexOf('/') - return lastSlash === 0 ? '/' : normalized.slice(0, lastSlash) - } - - ensureDirectoryExists(path) { - if (!this.directories.has(path)) { - this.mkdir(path, { recursive: true }) - } - } -} - -// Global Pyodide instance -let pyodide = null -let isInitialized = false -let initializationPromise = null - -// Global VFS instance -let vfs = null - -// Counter to detect corrupted package issues -let packageLoadFailures = 0 -let totalPackageLoadAttempts = 0 - -class SafePyRunner { - constructor() { - this.output = [] - this.loadedPackages = new Set() - this.executionStats = { - startTime: 0, - memoryUsage: 0, - operationCount: 0, - maxOperations: 100000, - maxMemory: 100 * 1024 * 1024 // 100MB limit - } - this.cdnOptions = [ - { - name: 'unpkg', - scriptURL: 'https://unpkg.com/pyodide@0.24.1/full/pyodide.js', - indexURL: 'https://unpkg.com/pyodide@0.24.1/full/' - }, - { - name: 'jsDelivr', - scriptURL: 'https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js', - indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.24.1/full/' - } - ] - this.activeCdnIndex = 0 - this.micropipReady = false - this.needsPyodideFsSync = false - this.preloadPackages = ['pandas'] - this.pyodidePackages = new Set([ - 'numpy', - 'pandas', - 'matplotlib', - 'scipy', - 'scikit-learn', - 'networkx', - 'sympy', - 'pillow', - 'requests', - 'beautifulsoup4', - 'seaborn', - 'plotly', - 'bokeh', - 'altair' - ]) - this.setupOutput() - this.setupVFS() - } - - setupVFS() { - // Initialize Virtual File System - if (!vfs) { - vfs = new SimpleVFS() - this.addOutput('info', 'Virtual file system initialized') - } - // Expose VFS on the worker global so Python `from js import vfs` works - self.vfs = vfs - } - - ensurePyodideFsDir(dirPath) { - if (!pyodide || !pyodide.FS) return - - const FS = pyodide.FS - const normalized = String(dirPath || '/') - if (normalized === '/' || normalized === '') return - - const parts = normalized.split('/').filter(Boolean) - let current = '' - for (const part of parts) { - current += `/${part}` - try { - FS.mkdir(current) - } catch (error) { - // Ignore EEXIST - } - } - } - - syncVfsToPyodideFs() { - if (!pyodide || !pyodide.FS || !vfs) return - - const FS = pyodide.FS - - // Ensure core directories exist - for (const dir of ['/data', '/workspace', '/tmp', '/uploads']) { - this.ensurePyodideFsDir(dir) - } - - // Mirror VFS directories into Pyodide FS - try { - for (const dir of vfs.directories) { - this.ensurePyodideFsDir(dir) - } - } catch (error) { - // Ignore directory sync errors - } - - let syncedFiles = 0 - for (const [path, content] of vfs.files.entries()) { - try { - const filePath = String(path) - const parentDir = filePath.substring(0, filePath.lastIndexOf('/')) || '/' - this.ensurePyodideFsDir(parentDir) - - if (content instanceof Uint8Array) { - FS.writeFile(filePath, content) - } else if (content instanceof ArrayBuffer) { - FS.writeFile(filePath, new Uint8Array(content)) - } else if (typeof content === 'string') { - FS.writeFile(filePath, content, { encoding: 'utf8' }) - } else { - FS.writeFile(filePath, String(content), { encoding: 'utf8' }) - } - - syncedFiles++ - } catch (error) { - this.addOutput('warn', `Failed to sync '${path}' to Pyodide FS: ${error.message}`) - } - } - - if (syncedFiles > 0) { - this.addOutput('debug', `Synced ${syncedFiles} file(s) to Pyodide FS`) - } - } - - setupOutput() { - // Capture stdout/stderr - this.capturedOutput = [] - this.outputCapture = { - write: (text) => { - this.capturedOutput.push(text) - this.addOutput('stdout', text) - }, - flush: () => {} - } - } - - addOutput(type, content) { - this.output.push({ - id: Date.now().toString() + Math.random().toString(36).substr(2, 9), - type: type, - content: String(content), - timestamp: new Date().toISOString() - }) - } - - // Initialize Pyodide with fallback CDNs - async initializePyodide() { - if (isInitialized) return pyodide - if (initializationPromise) return initializationPromise - - initializationPromise = new Promise(async (resolve, reject) => { - try { - this.addOutput('info', 'Initializing Python environment...') - - // Try multiple CDNs in case one is down or corrupted - const cdnOptions = this.cdnOptions - - let lastError = null - let loadedSuccessfully = false - - for (let index = 0; index < cdnOptions.length; index++) { - const cdn = cdnOptions[index] - try { - this.addOutput('info', `Trying Pyodide CDN: ${cdn.name}...`) - - // Load Pyodide script dynamically (only if not already loaded) - if (typeof self.loadPyodide === 'undefined') { - await new Promise((loadResolve, loadReject) => { - const hasDocument = typeof document !== 'undefined' && document && document.createElement - const script = hasDocument ? document.createElement('script') : null - if (!script) { - // In worker context, use importScripts (synchronous) - try { - importScripts(cdn.scriptURL) - loadResolve() - } catch (e) { - loadReject(e) - } - } else { - // In main thread context - script.src = cdn.scriptURL - script.onload = () => loadResolve() - script.onerror = () => loadReject(new Error(`Failed to load ${cdn.scriptURL}`)) - document.head.appendChild(script) - } - }) - } - - // Try to load Pyodide - pyodide = await loadPyodide({ - indexURL: cdn.indexURL, - stdout: this.outputCapture.write.bind(this.outputCapture), - stderr: this.outputCapture.write.bind(this.outputCapture) - }) - - this.addOutput('info', `✓ Successfully loaded Pyodide from ${cdn.name}`) - this.activeCdnIndex = index - loadedSuccessfully = true - break - } catch (error) { - lastError = error - this.addOutput('warn', `Failed to load from ${cdn.name}: ${error.message}`) - // Reset for next CDN attempt - pyodide = null - } - } - - if (!loadedSuccessfully || !pyodide) { - throw lastError || new Error('Failed to load Pyodide from any CDN') - } - - // Register VFS with Pyodide - pyodide.registerJsModule('vfs', vfs) - - try { - await pyodide.loadPackage('micropip') - this.micropipReady = true - for (const pkg of this.preloadPackages) { - try { - await this.installWithMicropip(pkg) - this.loadedPackages.add(pkg) - this.addOutput('info', `Preloaded package '${pkg}'`) - } catch (error) { - this.addOutput('warn', `Failed to preload '${pkg}': ${error.message}`) - } - } - } catch (error) { - this.addOutput('warn', `Failed to load micropip: ${error.message}`) - this.micropipReady = false - } - - // Set up matplotlib backend for web and VFS integration - await pyodide.runPython(` -import sys -import io -import os -import base64 -import json -import builtins -from contextlib import redirect_stdout, redirect_stderr -from pathlib import Path -import tempfile - -# Create custom output capture -class OutputCapture: - def __init__(self): - self.output = [] - - def write(self, text): - if text.strip(): - self.output.append(text) - - def flush(self): - pass - - def getvalue(self): - return ''.join(self.output) - -# Global output capture -_output_capture = OutputCapture() - -# Store original print function -_original_print = builtins.print - -# Custom print function -def custom_print(*args, **kwargs): - output = io.StringIO() - _original_print(*args, file=output, **kwargs) - result = output.getvalue() - _output_capture.write(result) - return result - -# Replace built-in print safely -builtins.print = custom_print - -# VFS Integration - File operations -class VFSFile: - """File-like object that interfaces with JavaScript VFS""" - def __init__(self, path, mode='r', encoding='utf-8'): - self.path = path - self.mode = mode - self.encoding = encoding - self.position = 0 - self.closed = False - self._content = None - - # Read existing content if file exists and mode allows reading - if 'r' in mode or 'a' in mode or '+' in mode: - try: - from js import vfs - if vfs.exists(path): - if 'b' in mode: - self._content = vfs.readFile(path, 'binary') - else: - self._content = vfs.readFile(path, 'utf8') - else: - if 'r' in mode and 'w' not in mode and 'a' not in mode and '+' not in mode: - raise FileNotFoundError(f"No such file or directory: '{path}'") - self._content = b'' if 'b' in mode else '' - except Exception as e: - # Avoid silently returning empty content for real errors (e.g. VFS not available) - if 'r' in mode and 'w' not in mode and 'a' not in mode and '+' not in mode: - raise - self._content = b'' if 'b' in mode else '' - else: - self._content = b'' if 'b' in mode else '' - - # Normalize JS Uint8Array for binary mode - if 'b' in mode: - try: - if hasattr(self._content, 'to_py'): - self._content = bytes(self._content.to_py()) - elif isinstance(self._content, list): - self._content = bytes(self._content) - elif isinstance(self._content, bytearray): - self._content = bytes(self._content) - except Exception: - pass - elif isinstance(self._content, (bytes, bytearray)): - try: - self._content = self._content.decode(self.encoding) - except Exception: - self._content = self._content.decode('utf-8', errors='ignore') - - # For write modes, truncate content - if 'w' in mode: - self._content = b'' if 'b' in mode else '' - - def read(self, size=-1): - if self.closed: - raise ValueError("I/O operation on closed file") - - if size == -1: - result = self._content[self.position:] - self.position = len(self._content) - else: - result = self._content[self.position:self.position + size] - self.position += len(result) - - return result - - def readline(self, size=-1): - if self.closed: - raise ValueError("I/O operation on closed file") - - if 'b' in self.mode: - newline = b'\\n' - else: - newline = '\\n' - - start = self.position - try: - newline_pos = self._content.index(newline, start) - if size == -1 or newline_pos - start + 1 <= size: - result = self._content[start:newline_pos + 1] - self.position = newline_pos + 1 - else: - result = self._content[start:start + size] - self.position = start + size - except ValueError: - # No newline found - if size == -1: - result = self._content[start:] - self.position = len(self._content) - else: - result = self._content[start:start + size] - self.position = start + size - - return result - - def readlines(self): - if self.closed: - raise ValueError("I/O operation on closed file") - - lines = [] - while True: - line = self.readline() - if not line: - break - lines.append(line) - return lines - - def write(self, data): - if self.closed: - raise ValueError("I/O operation on closed file") - - if 'r' in self.mode and '+' not in self.mode: - raise io.UnsupportedOperation("not writable") - - if 'b' in self.mode and isinstance(data, str): - data = data.encode(self.encoding) - elif 'b' not in self.mode and isinstance(data, bytes): - data = data.decode(self.encoding) - - if 'a' in self.mode: - # Append mode - self._content += data - self.position = len(self._content) - else: - # Write or insert mode - if isinstance(self._content, bytes): - self._content = self._content[:self.position] + data + self._content[self.position + len(data):] - else: - self._content = self._content[:self.position] + data + self._content[self.position + len(data):] - self.position += len(data) - - return len(data) - - def writelines(self, lines): - for line in lines: - self.write(line) - - def seek(self, position, whence=0): - if self.closed: - raise ValueError("I/O operation on closed file") - - if whence == 0: # SEEK_SET - self.position = position - elif whence == 1: # SEEK_CUR - self.position += position - elif whence == 2: # SEEK_END - self.position = len(self._content) + position - - self.position = max(0, min(self.position, len(self._content))) - return self.position - - def tell(self): - if self.closed: - raise ValueError("I/O operation on closed file") - return self.position - - def flush(self): - if self.closed: - raise ValueError("I/O operation on closed file") - - # Write content to VFS - try: - from js import vfs - if 'b' in self.mode: - vfs.writeFile(self.path, self._content, {'binary': True}) - else: - vfs.writeFile(self.path, self._content) - except Exception as e: - pass # Ignore VFS errors for now - - def close(self): - if not self.closed: - self.flush() - self.closed = True - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.close() - - def readable(self): - return 'r' in self.mode or '+' in self.mode - - def writable(self): - return 'w' in self.mode or 'a' in self.mode or '+' in self.mode - - def seekable(self): - return True - -# Override built-in open function -_original_open = builtins.open -import io -_original_io_open = io.open - -_VFS_ROOTS = ('/data', '/workspace', '/tmp', '/uploads') - -def vfs_open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None): - """VFS-aware open() replacement""" - # Convert file to string if it's a Path object - if hasattr(file, '__fspath__'): - file = file.__fspath__() - - file_str = str(file) - - # Check if this is a VFS path - is_vfs_path = file_str.startswith('/') and (file_str in _VFS_ROOTS or any(file_str.startswith(root + '/') for root in _VFS_ROOTS)) - is_write_mode = any(flag in mode for flag in ('w', 'a', '+', 'x')) - if is_vfs_path and not is_write_mode: - # Prefer Pyodide's native filesystem for reads (better compatibility with pandas, etc.) - try: - return _original_open(file, mode, buffering, encoding, errors, newline, closefd, opener) - except FileNotFoundError: - pass - - if encoding is None: - encoding = 'utf-8' - return VFSFile(file_str, mode, encoding) - elif is_vfs_path: - if encoding is None: - encoding = 'utf-8' - return VFSFile(file_str, mode, encoding) - else: - # Use original open for non-VFS paths - return _original_open(file, mode, buffering, encoding, errors, newline, closefd, opener) - -# Replace built-in open -builtins.open = vfs_open -io.open = vfs_open - -# Override os module functions for VFS -_original_listdir = os.listdir -_original_makedirs = os.makedirs -_original_path_exists = os.path.exists -_original_path_isfile = os.path.isfile -_original_path_isdir = os.path.isdir -_original_path_getsize = os.path.getsize -_original_getcwd = os.getcwd -_original_chdir = os.chdir -_original_remove = os.remove -_original_rmdir = os.rmdir - -def vfs_listdir(path='.'): - """VFS-aware listdir""" - try: - if str(path).startswith('/'): - from js import vfs - return vfs.readdir(str(path)) - else: - return _original_listdir(path) - except: - return _original_listdir(path) - -def vfs_makedirs(name, mode=0o777, exist_ok=False): - """VFS-aware makedirs""" - try: - if str(name).startswith('/'): - from js import vfs - vfs.mkdir(str(name), {'recursive': True}) - else: - _original_makedirs(name, mode, exist_ok) - except: - if not exist_ok: - raise - -def vfs_path_exists(path): - """VFS-aware path.exists""" - try: - if str(path).startswith('/'): - from js import vfs - return vfs.exists(str(path)) - else: - return _original_path_exists(path) - except: - return _original_path_exists(path) - -def vfs_path_isfile(path): - """VFS-aware path.isfile""" - try: - if str(path).startswith('/'): - from js import vfs - if vfs.exists(str(path)): - stat = vfs.stat(str(path)) - return stat.get('isFile', False) - return False - else: - return _original_path_isfile(path) - except: - return _original_path_isfile(path) - -def vfs_path_isdir(path): - """VFS-aware path.isdir""" - try: - if str(path).startswith('/'): - from js import vfs - if vfs.exists(str(path)): - stat = vfs.stat(str(path)) - return stat.get('isDirectory', False) - return False - else: - return _original_path_isdir(path) - except: - return _original_path_isdir(path) - -def vfs_getcwd(): - """VFS-aware getcwd""" - try: - from js import vfs - return vfs.getcwd() - except: - return '/workspace' - -def vfs_chdir(path): - """VFS-aware chdir""" - try: - if str(path).startswith('/'): - from js import vfs - vfs.chdir(str(path)) - else: - _original_chdir(path) - except: - pass - -def vfs_remove(path): - """VFS-aware remove""" - try: - if str(path).startswith('/'): - from js import vfs - vfs.unlink(str(path)) - else: - _original_remove(path) - except: - _original_remove(path) - -def vfs_rmdir(path): - """VFS-aware rmdir""" - try: - if str(path).startswith('/'): - from js import vfs - vfs.rmdir(str(path)) - else: - _original_rmdir(path) - except: - _original_rmdir(path) - -# Patch os module -os.listdir = vfs_listdir -os.makedirs = vfs_makedirs -os.path.exists = vfs_path_exists -os.path.isfile = vfs_path_isfile -os.path.isdir = vfs_path_isdir -os.getcwd = vfs_getcwd -os.chdir = vfs_chdir -os.remove = vfs_remove -os.rmdir = vfs_rmdir - -# Patch pathlib for modern Python code -try: - import pathlib - - class VFSPath(pathlib.PurePosixPath): - """VFS-aware Path implementation""" - - def exists(self): - return vfs_path_exists(str(self)) - - def is_file(self): - return vfs_path_isfile(str(self)) - - def is_dir(self): - return vfs_path_isdir(str(self)) - - def open(self, mode='r', buffering=-1, encoding=None, errors=None, newline=None): - return vfs_open(str(self), mode, buffering, encoding, errors, newline) - - def read_text(self, encoding=None, errors=None): - with self.open('r', encoding=encoding, errors=errors) as f: - return f.read() - - def read_bytes(self): - with self.open('rb') as f: - return f.read() - - def write_text(self, data, encoding=None, errors=None): - with self.open('w', encoding=encoding, errors=errors) as f: - return f.write(data) - - def write_bytes(self, data): - with self.open('wb') as f: - return f.write(data) - - def mkdir(self, mode=0o777, parents=False, exist_ok=False): - vfs_makedirs(str(self), mode, exist_ok or parents) - - def iterdir(self): - if self.is_dir(): - for name in vfs_listdir(str(self)): - yield self / name - - def glob(self, pattern): - # Simple glob implementation - try: - from js import vfs - matches = vfs.glob(str(self / pattern)) - return [VFSPath(match) for match in matches] - except: - return [] - - def unlink(self): - vfs_remove(str(self)) - - def rmdir(self): - vfs_rmdir(str(self)) - - # Replace Path with VFSPath for VFS paths - _original_Path = pathlib.Path - - def smart_Path(*args, **kwargs): - path_str = str(args[0]) if args else '.' - if path_str.startswith('/'): - return VFSPath(*args, **kwargs) - else: - return _original_Path(*args, **kwargs) - - pathlib.Path = smart_Path - -except ImportError: - pass # pathlib not available - -# Set initial working directory -try: - from js import vfs - vfs.chdir('/workspace') -except: - pass -`) - - isInitialized = true - this.addOutput('info', 'Python environment initialized successfully') - resolve(pyodide) - } catch (error) { - this.addOutput('error', `Failed to initialize Python: ${error.message}`) - reject(error) - } - }) - - return initializationPromise - } - - // Get available Python packages - getAvailablePackages() { - return { - 'numpy': 'Numerical computing library', - 'pandas': 'Data manipulation and analysis', - 'matplotlib': 'Plotting library', - 'scipy': 'Scientific computing', - 'scikit-learn': 'Machine learning library', - 'requests': 'HTTP library', - 'beautifulsoup4': 'Web scraping', - 'pillow': 'Image processing', - 'sympy': 'Symbolic mathematics', - 'networkx': 'Graph analysis', - 'seaborn': 'Statistical visualization', - 'plotly': 'Interactive plotting', - 'bokeh': 'Interactive visualization', - 'altair': 'Statistical visualization' - } - } - - isPackageNotFoundError(error) { - if (!error || !error.message) return false - const message = error.message.toLowerCase() - return message.includes('404') || - message.includes('not found') || - message.includes('unknown package') || - message.includes('no such file') || - message.includes('does not exist') - } - - makeCacheBustingFetch(attempt) { - return async (url, ...args) => { - const separator = url.includes('?') ? '&' : '?' - const cacheBustedUrl = `${url}${separator}cachebust=${Date.now()}_${attempt}` - const response = await fetch(cacheBustedUrl, ...args) - const contentType = response.headers.get('content-type') || '' - const isWheel = url.endsWith('.whl') || url.includes('.whl?') - - if (isWheel && contentType.includes('application/wasm')) { - this.addOutput('warn', `Unexpected content-type for wheel: ${contentType} from ${url}`) - const alt = this.getAlternateCdnUrl(url) - if (alt) { - this.addOutput('info', `Retrying wheel download from alternate CDN: ${alt}`) - return fetch(alt, ...args) - } - } - - return response - } - } - - getAlternateCdnUrl(url) { - const current = this.cdnOptions[this.activeCdnIndex] - if (!current || !current.indexURL || !url.startsWith(current.indexURL)) return null - const fallbackIndex = (this.activeCdnIndex + 1) % this.cdnOptions.length - const fallback = this.cdnOptions[fallbackIndex] - if (!fallback || !fallback.indexURL) return null - return url.replace(current.indexURL, fallback.indexURL) - } - - async ensureMicropip() { - if (this.micropipReady) return true - try { - await pyodide.loadPackage('micropip') - this.micropipReady = true - return true - } catch (error) { - this.addOutput('warn', `Failed to load micropip: ${error.message}`) - this.micropipReady = false - return false - } - } - - async installWithMicropip(packageName) { - const ready = await this.ensureMicropip() - if (!ready) { - throw new Error('micropip is not available') - } - - const pkg = JSON.stringify(packageName) - await pyodide.runPythonAsync(` -import micropip -await micropip.install(${pkg}) -`) - } - - async installWithPyodide(packageName, attempt) { - const options = attempt > 1 ? { fetch: this.makeCacheBustingFetch(attempt) } : undefined - await pyodide.loadPackage(packageName, options) - } - - // Load a Python package with retry logic and cache busting - async loadPackage(packageName, retries = 3) { - if (!pyodide) { - await this.initializePyodide() - } - - if (this.loadedPackages.has(packageName)) { - return true - } - - let lastError = null - for (let attempt = 1; attempt <= retries; attempt++) { - try { - totalPackageLoadAttempts++ - this.addOutput('info', `Loading Python package: ${packageName}${attempt > 1 ? ` (attempt ${attempt}/${retries})` : ''}`) - - if (this.pyodidePackages.has(packageName)) { - await this.installWithPyodide(packageName, attempt) - } else { - await this.installWithMicropip(packageName) - } - this.loadedPackages.add(packageName) - this.addOutput('info', `Package '${packageName}' loaded successfully`) - return true - } catch (error) { - lastError = error - const isNotFound = this.isPackageNotFoundError(error) - if (!isNotFound) { - packageLoadFailures++ - } - this.addOutput('warn', `Attempt ${attempt}/${retries} failed for '${packageName}': ${error.message}`) - - if (attempt < retries) { - // Exponential backoff: wait 1s, 2s, 4s, etc. - const waitTime = Math.pow(2, attempt - 1) * 1000 - this.addOutput('info', `Retrying in ${waitTime / 1000}s...`) - await new Promise(resolve => setTimeout(resolve, waitTime)) - - // Clear pyodide's internal package cache on retry - try { - if (pyodide.package_loader && pyodide.package_loader.loadedPackages) { - delete pyodide.package_loader.loadedPackages[packageName] - } - } catch (e) { - // Ignore cache clear errors - } - } - } - } - - // Check if this is a systematic failure (most packages failing) - const failureRate = totalPackageLoadAttempts > 0 ? packageLoadFailures / totalPackageLoadAttempts : 0 - const isSystematicFailure = failureRate > 0.7 && totalPackageLoadAttempts >= 3 - - // Provide detailed troubleshooting instructions - this.addOutput('error', `Failed to load package '${packageName}' after ${retries} attempts`) - this.addOutput('error', `Last error: ${lastError?.message || 'Unknown error'}`) - - if (this.isPackageNotFoundError(lastError)) { - this.addOutput('error', `⚠️ PACKAGE NOT FOUND IN PYODIDE INDEX`) - this.addOutput('error', `The package '${packageName}' is not available in the Pyodide package repository.`) - this.addOutput('info', `If you need this package, consider using micropip with a compatible wheel.`) - } else if (isSystematicFailure) { - this.addOutput('error', `⚠️ DETECTED SYSTEMATIC PACKAGE FAILURE (${Math.round(failureRate * 100)}% failure rate)`) - this.addOutput('error', `All packages are being corrupted during download. This is likely:`) - this.addOutput('error', ` • Corporate proxy/firewall inspecting and corrupting .whl files`) - this.addOutput('error', ` • Browser extension interfering with downloads`) - this.addOutput('error', ` • Network issue corrupting binary downloads`) - this.addOutput('error', ``) - this.addOutput('error', `TO FIX THIS ISSUE:`) - this.addOutput('error', ` 1. Try disabling ALL browser extensions`) - this.addOutput('error', ` 2. Try a different browser (Chrome, Firefox, Safari)`) - this.addOutput('error', ` 3. Try a different network (WiFi hotspot, mobile data)`) - this.addOutput('error', ` 4. If using corporate VPN/proxy, try disconnecting`) - this.addOutput('error', ` 5. Check if antivirus is scanning web downloads`) - this.addOutput('error', ` 6. Try in incognito/private mode (extensions disabled)`) - this.addOutput('info', `💡 Python standard library will still work (math, json, re, datetime, etc.)`) - this.addOutput('info', ` Only external packages like numpy/pandas require download.`) - } else { - this.addOutput('info', `📋 Troubleshooting steps:`) - this.addOutput('info', ` 1. Hard refresh: Cmd+Shift+R (Mac) or Ctrl+Shift+R (Windows)`) - this.addOutput('info', ` 2. Clear browser cache for this site`) - this.addOutput('info', ` 3. Check your internet connection`) - this.addOutput('info', ` 4. Try a different browser or network`) - this.addOutput('info', ` 5. If problem persists, the CDN may be temporarily unavailable`) - } - - return false - } - - // Parse code for package import statements - parsePackageImports(code) { - const imports = [] - - // Match import statements - const importRegex = /(?:^|\n)\s*(?:import|from)\s+([a-zA-Z_][a-zA-Z0-9_]*)/gm - let match - - while ((match = importRegex.exec(code)) !== null) { - const packageName = match[1] - - // Map common package names to Pyodide package names - const packageMapping = { - 'numpy': 'numpy', - 'np': 'numpy', - 'pandas': 'pandas', - 'pd': 'pandas', - 'matplotlib': 'matplotlib', - 'plt': 'matplotlib', - 'scipy': 'scipy', - 'sklearn': 'scikit-learn', - 'requests': 'requests', - 'bs4': 'beautifulsoup4', - 'PIL': 'pillow', - 'sympy': 'sympy', - 'networkx': 'networkx', - 'seaborn': 'seaborn', - 'plotly': 'plotly', - 'bokeh': 'bokeh', - 'altair': 'altair' - } - - const mappedName = packageMapping[packageName] || packageName - if (this.getAvailablePackages()[mappedName]) { - imports.push(mappedName) - } else if (!this.isStdlibPackage(mappedName)) { - imports.push(mappedName) - } - } - - return [...new Set(imports)] // Remove duplicates - } - - isStdlibPackage(name) { - const stdlib = new Set([ - 'abc', 'argparse', 'array', 'asyncio', 'base64', 'binascii', 'bisect', - 'calendar', 'collections', 'contextlib', 'copy', 'csv', 'dataclasses', - 'datetime', 'decimal', 'difflib', 'enum', 'functools', 'gc', 'hashlib', - 'heapq', 'hmac', 'html', 'http', 'io', 'itertools', 'json', 'logging', - 'math', 'numbers', 'operator', 'os', 'pathlib', 'pickle', 'platform', - 'queue', 'random', 're', 'shlex', 'signal', 'socket', 'sqlite3', - 'statistics', 'string', 'struct', 'subprocess', 'sys', 'tempfile', - 'textwrap', 'threading', 'time', 'types', 'typing', 'unicodedata', - 'urllib', 'uuid', 'warnings', 'weakref', 'zipfile' - ]) - return stdlib.has(name) - } - - // Handle matplotlib plots - async handleMatplotlib(code) { - if (!code.includes('matplotlib') && !code.includes('plt')) { - return null - } - - try { - // Set up matplotlib for web output - await pyodide.runPython(` -import matplotlib -matplotlib.use('Agg') # Use non-interactive backend -import matplotlib.pyplot as plt -import io -import base64 - -# Clear any existing plots -plt.clf() - -# Function to capture plot as base64 -def capture_plot(): - buf = io.BytesIO() - plt.savefig(buf, format='png', bbox_inches='tight', dpi=150) - buf.seek(0) - img_base64 = base64.b64encode(buf.read()).decode('utf-8') - buf.close() - return img_base64 -`) - - // Execute the user code - await pyodide.runPython(code) - - // Check if there's a plot to capture - const hasPlot = await pyodide.runPython(` -import matplotlib.pyplot as plt -len(plt.get_fignums()) > 0 -`) - - if (hasPlot) { - const plotData = await pyodide.runPython('capture_plot()') - return { - type: 'matplotlib', - data: plotData, - format: 'png' - } - } - - return null - } catch (error) { - this.addOutput('error', `Matplotlib error: ${error.message}`) - return null - } - } - - // Resource monitoring - checkResourceLimits() { - this.executionStats.operationCount++ - - if (this.executionStats.operationCount > this.executionStats.maxOperations) { - throw new Error(`Operation limit exceeded: ${this.executionStats.maxOperations} operations`) - } - - // Estimate memory usage - const memoryEstimate = this.output.length * 1000 + this.loadedPackages.size * 10000000 - if (memoryEstimate > this.executionStats.maxMemory) { - throw new Error(`Memory limit exceeded: ~${Math.round(memoryEstimate / 1024 / 1024)}MB`) - } - - this.executionStats.memoryUsage = memoryEstimate - } - - // Execute Python code - async execute(code) { - this.output = [] - this.capturedOutput = [] - this.executionStats.startTime = performance.now() - this.executionStats.operationCount = 0 - this.executionStats.memoryUsage = 0 - - try { - // Initialize Pyodide if not already done - if (!pyodide) { - await this.initializePyodide() - } - - if (this.needsPyodideFsSync) { - try { - this.syncVfsToPyodideFs() - } catch (error) { - this.addOutput('warn', `Pyodide FS sync failed: ${error.message}`) - } - this.needsPyodideFsSync = false - } - - // Parse and load required packages - const requiredPackages = this.parsePackageImports(code) - for (const pkg of requiredPackages) { - const success = await this.loadPackage(pkg) - if (!success) { - throw new Error(`Failed to load required package '${pkg}'. Please try again.`) - } - } - - // Clear previous output - await pyodide.runPython(` -_output_capture.output = [] -`) - - // Handle matplotlib plots - const plotResult = await this.handleMatplotlib(code) - if (plotResult) { - this.addOutput('matplotlib', JSON.stringify(plotResult)) - } - - // Execute the code and capture result - let result = null - try { - // Handle asyncio.run() which doesn't work in Pyodide's existing event loop - let processedCode = code - if (code.includes('asyncio.run(')) { - this.addOutput('info', 'Detected asyncio.run() - converting for Pyodide compatibility') - - // Replace asyncio.run(func()) with await func() wrapped in an async context - processedCode = code.replace(/asyncio\.run\(([^)]+)\)/g, 'await $1') - - // Wrap the entire code in an async function that can be executed by runPythonAsync - processedCode = ` -import asyncio - -async def _execute_main(): -${processedCode.split('\n').map(line => ' ' + line).join('\n')} - -# Execute the main function -await _execute_main() -` - - this.addOutput('info', 'Wrapped code in async context for Pyodide execution') - } - - // Check if code has async/await but no asyncio.run - else if (processedCode.includes('async def') || processedCode.includes('await ')) { - // Wrap async code in a proper async context - processedCode = ` -import asyncio -try: - loop = asyncio.get_event_loop() -except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - -${processedCode} -` - } - - // Use runPythonAsync for async code, runPython for sync code - if (processedCode.includes('asyncio.') || processedCode.includes('async def') || processedCode.includes('await ')) { - result = await pyodide.runPythonAsync(processedCode) - } else { - result = await pyodide.runPython(processedCode) - } - - // Get captured output - const capturedOutput = await pyodide.runPython(` -_output_capture.getvalue() -`) - - if (capturedOutput && capturedOutput.trim()) { - this.addOutput('stdout', capturedOutput) - } - - // Clear output buffer - await pyodide.runPython(` -_output_capture.output = [] -`) - - // Add result if it exists - if (result !== undefined && result !== null) { - this.addOutput('return', String(result)) - } - } catch (error) { - // Get any remaining output before error - try { - const capturedOutput = await pyodide.runPython(`_output_capture.getvalue()`) - if (capturedOutput && capturedOutput.trim()) { - this.addOutput('stdout', capturedOutput) - } - } catch (e) { - // Ignore output capture errors - } - - throw error - } - - - // Add execution statistics - const executionTime = Math.round(performance.now() - this.executionStats.startTime) - const memoryUsed = Math.round(this.executionStats.memoryUsage / 1024 / 1024 * 100) / 100 - const operations = this.executionStats.operationCount - - this.addOutput('info', `Python execution completed in ${executionTime}ms | ~${memoryUsed}MB | ${operations} ops`) - - return this.output - } catch (error) { - const executionTime = Math.round(performance.now() - this.executionStats.startTime) - const memoryUsed = Math.round(this.executionStats.memoryUsage / 1024 / 1024 * 100) / 100 - const operations = this.executionStats.operationCount - - this.addOutput('error', `${error.message}`) - this.addOutput('info', `Python execution failed after ${executionTime}ms | ~${memoryUsed}MB | ${operations} ops`) - return this.output - } - } -} - -// Create runner instance -const runner = new SafePyRunner() - -// Handle messages from main thread -self.onmessage = async (e) => { - const { type, code, timeout = 30000, requestId, packageName, path, data, options } = e.data - - try { - if (type === 'execute') { - // Set up execution timeout - let timeoutId - const timeoutPromise = new Promise((_, reject) => { - timeoutId = setTimeout(() => { - reject(new Error(`Python execution timed out after ${timeout}ms`)) - }, timeout) - }) - - // Execute code with timeout - const executionPromise = runner.execute(code) - - const results = await Promise.race([executionPromise, timeoutPromise]) - - // Clear timeout if execution completed in time - clearTimeout(timeoutId) - - // Send results back to main thread - self.postMessage({ - type: 'results', - data: results, - requestId: requestId - }) - } else if (type === 'loadPackage') { - // Handle package loading requests - const success = await runner.loadPackage(packageName) - self.postMessage({ - type: 'packageLoaded', - data: { success, packageName }, - requestId: requestId - }) - } else if (type === 'getAvailablePackages') { - // Handle package list requests - const packages = runner.getAvailablePackages() - self.postMessage({ - type: 'availablePackages', - data: packages, - requestId: requestId - }) - } else if (type === 'initialize') { - // Handle initialization requests - await runner.initializePyodide() - self.postMessage({ - type: 'initialized', - data: { success: true }, - requestId: requestId - }) - } else if (type === 'vfs') { - // Handle VFS operations - const { operation } = e.data - let result - - try { - switch (operation) { - case 'writeFile': - result = vfs.writeFile(path, data, options) - break - case 'readFile': - result = vfs.readFile(path, options?.encoding) - break - case 'exists': - result = vfs.exists(path) - break - case 'mkdir': - result = vfs.mkdir(path, options) - break - case 'readdir': - result = vfs.readdir(path) - break - case 'stat': - result = vfs.stat(path) - break - case 'unlink': - result = vfs.unlink(path) - break - case 'rmdir': - result = vfs.rmdir(path) - break - case 'chdir': - result = vfs.chdir(path) - break - case 'getcwd': - result = vfs.getcwd() - break - default: - throw new Error(`Unknown VFS operation: ${operation}`) - } - - self.postMessage({ - type: 'vfsResult', - data: result, - requestId: requestId - }) - } catch (error) { - self.postMessage({ - type: 'vfsError', - data: { message: error.message }, - requestId: requestId - }) - } - } else if (type === 'syncVFS') { - // Handle VFS synchronization from main thread - try { - const { vfsState } = e.data - if (vfsState && vfsState.files && vfsState.directories) { - // Clear current VFS state - vfs.files.clear() - vfs.directories.clear() - vfs.metadata.clear() - - // Import files from serialized state (array of [path, content] pairs) - if (Array.isArray(vfsState.files)) { - vfsState.files.forEach(([path, content]) => { - vfs.files.set(path, content) - }) - } - - // Import directories from serialized state (array of paths) - if (Array.isArray(vfsState.directories)) { - vfsState.directories.forEach(dir => { - vfs.directories.add(dir) - }) - } - - // Import metadata from serialized state (array of [path, meta] pairs) - if (Array.isArray(vfsState.metadata)) { - vfsState.metadata.forEach(([path, meta]) => { - // Convert ISO strings back to Date objects - const processedMeta = { - ...meta, - mtime: meta.mtime && typeof meta.mtime === 'string' ? new Date(meta.mtime) : meta.mtime, - created: meta.created && typeof meta.created === 'string' ? new Date(meta.created) : meta.created - } - vfs.metadata.set(path, processedMeta) - }) - } - - // Update current directory - if (vfsState.currentDirectory) { - vfs.currentDirectory = vfsState.currentDirectory - } - - runner.addOutput('info', `VFS synchronized: ${vfs.files.size} files, ${vfs.directories.size} directories`) - - runner.needsPyodideFsSync = true - if (pyodide) { - try { - runner.syncVfsToPyodideFs() - runner.needsPyodideFsSync = false - } catch (error) { - runner.addOutput('warn', `Pyodide FS sync failed: ${error.message}`) - } - } - } - } catch (error) { - runner.addOutput('error', `VFS sync failed: ${error.message}`) - } - } else { - // Backward compatibility - treat as execute - const executionPromise = runner.execute(code) - const results = await executionPromise - - self.postMessage({ - type: 'results', - data: results, - requestId: requestId - }) - } - } catch (error) { - // Handle timeout or other errors - self.postMessage({ - type: 'error', - data: { - message: error.message, - name: error.name || 'ExecutionError' - }, - requestId: requestId - }) - } -} - -// Handle worker errors -self.onerror = (error) => { - self.postMessage({ - type: 'error', - data: { - message: error.message || 'Unknown Python worker error', - name: 'WorkerError' - } - }) -} - -// Handle unhandled promise rejections -self.onunhandledrejection = (event) => { - self.postMessage({ - type: 'error', - data: { - message: event.reason?.message || 'Unhandled promise rejection in Python worker', - name: 'UnhandledRejection' - } - }) -} diff --git a/web/src/api/chat_instructions.ts b/web/src/api/chat_instructions.ts index 53523445..bb44ae2c 100644 --- a/web/src/api/chat_instructions.ts +++ b/web/src/api/chat_instructions.ts @@ -2,7 +2,6 @@ import request from '@/utils/request/axios' export interface ChatInstructions { artifactInstruction: string - toolInstruction: string } export const fetchChatInstructions = async (): Promise => { diff --git a/web/src/api/chat_session.ts b/web/src/api/chat_session.ts index eb369fbc..e2cf9e73 100644 --- a/web/src/api/chat_session.ts +++ b/web/src/api/chat_session.ts @@ -17,9 +17,7 @@ export const getChatSessionDefault = async (title: string): Promise -
- - - - -
- -

Files uploaded here will be immediately available in Python and JavaScript code runners. -

-

Access them using standard file operations like pd.read_csv('/data/filename.csv') or - fs.readFileSync('/data/filename.csv') -

-
- - -
- Upload to: - - - - 📊 /data (CSV, JSON, datasets) - - - 💼 /workspace (code, docs) - - - 📤 /uploads (general files) - - - -
- - - - -
- - - - - Click or drag files here to upload - - - Supports: Data files (CSV, JSON, Excel), Code files (Python, JavaScript, TypeScript), Documents - (Markdown, - Text), Images (PNG, JPG, SVG), Archives (ZIP, TAR) - -
-
-
- - -
- Upload Results -
-
- - - - -
-
{{ result.filename }}
-
{{ result.path }}
-
{{ result.message }}
-
-
-
- - -
- How to use uploaded files: - - - - - - Copy Python Code - - - - - - - Copy JavaScript Code - - - -
-
- - -
- Current VFS Status -
-
- {{ vfsStats.totalFiles }} - Files -
-
- {{ formatSize(vfsStats.totalSize) }} - Used -
-
- {{ Math.round((vfsStats.totalSize / (100 * 1024 * 1024)) * 100) }}% - Quota -
-
-
-
- - -
- - - - - -
- - Loading file manager... -
- -
-
-
- - - - - diff --git a/web/src/components/VFSDemo.vue b/web/src/components/VFSDemo.vue deleted file mode 100644 index 76d1a731..00000000 --- a/web/src/components/VFSDemo.vue +++ /dev/null @@ -1,472 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/VFSFileManager.vue b/web/src/components/VFSFileManager.vue deleted file mode 100644 index 7f9969ee..00000000 --- a/web/src/components/VFSFileManager.vue +++ /dev/null @@ -1,815 +0,0 @@ - - - - - diff --git a/web/src/components/VFSFileUploader.vue b/web/src/components/VFSFileUploader.vue deleted file mode 100644 index c039b111..00000000 --- a/web/src/components/VFSFileUploader.vue +++ /dev/null @@ -1,456 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/VFSIntegration.vue b/web/src/components/VFSIntegration.vue deleted file mode 100644 index 7c090148..00000000 --- a/web/src/components/VFSIntegration.vue +++ /dev/null @@ -1,461 +0,0 @@ - - - - - \ No newline at end of file diff --git a/web/src/components/VFSProvider.vue b/web/src/components/VFSProvider.vue deleted file mode 100644 index 91976aaa..00000000 --- a/web/src/components/VFSProvider.vue +++ /dev/null @@ -1,334 +0,0 @@ - - - - - diff --git a/web/src/locales/en-US.json b/web/src/locales/en-US.json index 8e7e018f..9accbaf4 100644 --- a/web/src/locales/en-US.json +++ b/web/src/locales/en-US.json @@ -114,8 +114,6 @@ "clearChat": "Clear chat session", "clearChatConfirm": "Do you want to clear the chat session?", "clearHistoryConfirm": "Are you sure you want to clear the chat history?", - "codeRunner": "Code Runner", - "codeRunnerDescription": "Execute code snippets in a sandboxed environment", "commentFailed": "Failed to add comment", "commentPlaceholder": "Enter your comment...", "commentSuccess": "Comment added successfully", @@ -133,11 +131,9 @@ "deleteMessage": "Delete message", "deleteMessageConfirm": "Do you want to delete this message?", "disable_debug": "Disable", - "disable_code_runner": "Disable", "disable_artifact": "Disable", "disable_explore": "Disable", "enable_debug": "Enable", - "enable_code_runner": "Enable", "enable_artifact": "Enable", "enable_explore": "Enable", "exploreMode": "Explore Mode", @@ -172,10 +168,6 @@ "suggestedQuestions": "Suggested Questions", "summarize_mode": "Summary mode (supports longer context 20+)", "temperature": "Temperature : {temperature}", - "toolDebug": "Tool Debug", - "toolDebugDescription": "Show detailed tool execution logs", - "toolInstructionTitle": "Tool Instructions", - "toolRunning": "Running tool...", "topP": "Top P: {topP}", "turnOffContext": "In this mode, messages sent will not include previous chat logs.", "turnOnContext": "In this mode, messages sent will include previous chat logs.", diff --git a/web/src/locales/zh-CN.json b/web/src/locales/zh-CN.json index 46df549a..2d6fe3cd 100644 --- a/web/src/locales/zh-CN.json +++ b/web/src/locales/zh-CN.json @@ -84,7 +84,6 @@ "alreadyInNewChat": "已经在新对话中", "advanced_settings": "高级设置", "artifactModeDescription": "启用代码、预览和可视化的 Artifact 渲染", - "codeRunnerDescription": "在沙盒环境中执行代码片段", "debugDescription": "启用调试模式用于故障排除和诊断", "defaultSystemPrompt": "你是一个有帮助且简明的助手。需要时先提出澄清问题。给出准确答案,并提供简短理由和可执行步骤。不确定时要说明,并建议如何验证。", "exploreModeDescription": "基于对话上下文获取建议问题", @@ -133,24 +132,17 @@ "frequencyPenalty": "频率惩罚", "presencePenalty": "存在惩罚", "debug": "调试模式", - "codeRunner": "代码运行器", "artifactMode": "Artifacts", - "toolDebug": "工具调试", - "toolDebugDescription": "显示详细的工具执行日志", - "toolRunning": "工具运行中...", "sessionConfig": "会话设置", "enable_debug": "启用", - "enable_code_runner": "启用", "enable_artifact": "启用", "disable_debug": "关闭", - "disable_code_runner": "关闭", "disable_artifact": "关闭", "exploreMode": "探索模式", "enable_explore": "启用", "disable_explore": "关闭", "promptInstructions": "提示词说明", "artifactInstructionTitle": "Artifact 说明", - "toolInstructionTitle": "工具说明", "loading_instructions": "正在加载说明...", "loadingSession": "正在加载会话...", "completionsCount": "结果数量: {contextCount}", diff --git a/web/src/locales/zh-TW.json b/web/src/locales/zh-TW.json index 3d0f5160..c5b3c1c5 100644 --- a/web/src/locales/zh-TW.json +++ b/web/src/locales/zh-TW.json @@ -103,7 +103,6 @@ "alreadyInNewChat": "已在新的對話中", "artifactModeDescription": "啟用代碼、預覽和可視化的 Artifact 渲染", "chatSettings": "對話設定", - "codeRunnerDescription": "在沙盒環境中執行代碼片段", "debugDescription": "啟用調試模式用於故障排除和診斷", "defaultSystemPrompt": "你是一個有幫助且簡明的助手。需要時先提出澄清問題。給出準確答案,並提供簡短理由和可執行步驟。不確定時要說明,並建議如何驗證。", "exploreModeDescription": "基於對話上下文獲取建議問題", @@ -125,26 +124,19 @@ "copyCode": "複製代碼", "createBot": "建立機器人", "debug": "調試模式", - "codeRunner": "程式執行器", "artifactMode": "Artifacts", - "toolDebug": "工具除錯", - "toolDebugDescription": "顯示詳細的工具執行日誌", - "toolRunning": "工具執行中...", "deleteChatSessionsConfirm": "確定刪除此記錄?", "deleteMessage": "刪除消息", "deleteMessageConfirm": "是否刪除此消息?", "disable_debug": "關閉", - "disable_code_runner": "關閉", "disable_artifact": "關閉", "disable_explore": "關閉", "enable_debug": "開啟", - "enable_code_runner": "開啟", "enable_artifact": "開啟", "enable_explore": "開啟", "exploreMode": "探索模式", "promptInstructions": "提示詞說明", "artifactInstructionTitle": "Artifact 說明", - "toolInstructionTitle": "工具說明", "loading_instructions": "正在載入說明...", "loadingSession": "正在載入會話...", "exportFailed": "導出失敗", diff --git a/web/src/services/codeRunner.ts b/web/src/services/codeRunner.ts deleted file mode 100644 index 2e655b0d..00000000 --- a/web/src/services/codeRunner.ts +++ /dev/null @@ -1,776 +0,0 @@ -/** - * Code Runner Service - * Handles execution of JavaScript code in a safe Web Worker environment - */ - - -import { executionHistory } from './executionHistory' - -export interface ExecutionResult { - id: string - type: 'log' | 'error' | 'return' | 'stdout' | 'warn' | 'info' | 'debug' | 'canvas' | 'matplotlib' - content: string - timestamp: string - execution_time_ms?: number -} - -export interface LibraryInfo { - [key: string]: string -} - -export interface CanvasOperation { - type: 'canvas' - width: number - height: number - operations: any[] -} - -export interface ExecutionResponse { - results: ExecutionResult[] - status: 'success' | 'error' | 'timeout' - execution_time_ms: number -} - -export class CodeRunner { - private jsWorker: Worker | null = null - - private pyWorker: Worker | null = null - - private requestCounter = 0 - private pendingRequests = new Map void - reject: (error: Error) => void - timeout?: NodeJS.Timeout - }>() - - private vfsInstance: any = null - - constructor() { - - this.initializeWorkers() - } - - /** - * Set the VFS instance to synchronize with workers - */ - setVFSInstance(vfs: any) { - this.vfsInstance = vfs - } - - /** - * Synchronize VFS state with workers - */ - async syncVFSToWorkers() { - if (!this.vfsInstance) return - - try { - // Get all files from VFS - const vfsState = this.vfsInstance.export() - - // Convert Map/Set to plain objects for worker transfer - // Also convert Date objects to ISO strings for serialization - const metadataEntries = Array.from(vfsState.metadata.entries()) as Array<[string, any]> - const serializedState = { - files: Array.from(vfsState.files.entries()), - directories: Array.from(vfsState.directories), - metadata: metadataEntries.map(([path, meta]) => [ - path, - { - ...meta, - mtime: meta.mtime instanceof Date ? meta.mtime.toISOString() : meta.mtime, - created: meta.created instanceof Date ? meta.created.toISOString() : meta.created - } - ]), - currentDirectory: vfsState.currentDirectory - } - - // Send VFS state to both workers - if (this.jsWorker) { - this.jsWorker.postMessage({ - type: 'syncVFS', - vfsState: serializedState - }) - } - - if (this.pyWorker) { - this.pyWorker.postMessage({ - type: 'syncVFS', - vfsState: serializedState - }) - } - } catch (error) { - console.error('Failed to sync VFS to workers:', error) - } - } - - private initializeWorkers() { - try { - // Initialize JavaScript worker - this.jsWorker = new Worker('/workers/jsRunner.js') - this.jsWorker.onmessage = this.handleWorkerMessage.bind(this) - this.jsWorker.onerror = this.handleWorkerError.bind(this) - - // Initialize Python worker - this.pyWorker = new Worker('/workers/pyRunner.js') - this.pyWorker.onmessage = this.handleWorkerMessage.bind(this) - this.pyWorker.onerror = this.handleWorkerError.bind(this) - } catch (error) { - console.error('Failed to initialize workers:', error) - - } - } - - private handleWorkerMessage(e: MessageEvent) { - const { type, data, requestId } = e.data - - const pendingRequest = this.pendingRequests.get(requestId) - if (!pendingRequest) return - - // Clear timeout - if (pendingRequest.timeout) { - clearTimeout(pendingRequest.timeout) - } - - // Remove from pending requests - this.pendingRequests.delete(requestId) - - if (type === 'results') { - pendingRequest.resolve(data) - } else if (type === 'libraryLoaded') { - pendingRequest.resolve(data) - } else if (type === 'availableLibraries') { - pendingRequest.resolve(data) - } else if (type === 'packageLoaded') { - pendingRequest.resolve(data) - } else if (type === 'availablePackages') { - pendingRequest.resolve(data) - } else if (type === 'initialized') { - pendingRequest.resolve(data) - } else if (type === 'vfsResult') { - pendingRequest.resolve(data) - } else if (type === 'vfsError') { - pendingRequest.reject(new Error(data.message || 'VFS operation failed')) - } else if (type === 'error') { - pendingRequest.reject(new Error(data.message || 'Unknown execution error')) - } - } - - private handleWorkerError(error: ErrorEvent) { - - console.error('Worker error:', error) - - - // Reject all pending requests - for (const [requestId, request] of this.pendingRequests) { - if (request.timeout) { - clearTimeout(request.timeout) - } - request.reject(new Error('Worker crashed: ' + error.message)) - } - this.pendingRequests.clear() - - - // Reinitialize workers - this.dispose() - this.initializeWorkers() - - } - - private generateRequestId(): string { - return `req_${++this.requestCounter}_${Date.now()}` - } - - /** - * Execute JavaScript code - */ - async executeJavaScript(code: string, timeoutMs = 10000): Promise { - if (!this.jsWorker) { - throw new Error('JavaScript worker not available') - } - - const requestId = this.generateRequestId() - - return new Promise((resolve, reject) => { - // Set up timeout - const timeout = setTimeout(() => { - this.pendingRequests.delete(requestId) - reject(new Error(`Code execution timed out after ${timeoutMs}ms`)) - }, timeoutMs) - - // Store pending request - this.pendingRequests.set(requestId, { - resolve, - reject, - timeout - }) - - // Send code to worker - this.jsWorker!.postMessage({ - type: 'execute', - code, - timeout: timeoutMs, - requestId - }) - }) - } - - /** - - * Execute Python code - */ - async executePython(code: string, timeoutMs = 30000): Promise { - if (!this.pyWorker) { - throw new Error('Python worker not available') - } - - const requestId = this.generateRequestId() - - return new Promise((resolve, reject) => { - // Set up timeout - const timeout = setTimeout(() => { - this.pendingRequests.delete(requestId) - reject(new Error(`Python execution timed out after ${timeoutMs}ms`)) - }, timeoutMs) - - // Store pending request - this.pendingRequests.set(requestId, { - resolve, - reject, - timeout - }) - - // Send code to worker - this.pyWorker!.postMessage({ - type: 'execute', - code, - timeout: timeoutMs, - requestId - }) - }) - } - - /** - * Execute code based on language - */ - async execute(language: string, code: string, artifactId?: string): Promise { - - const startTime = performance.now() - - // Sync VFS to workers before execution - await this.syncVFSToWorkers() - - try { - let results: ExecutionResult[] - - switch (language.toLowerCase()) { - case 'javascript': - case 'js': - case 'typescript': - case 'ts': - results = await this.executeJavaScript(code) - break - case 'python': - case 'py': - results = await this.executePython(code) - break - default: - throw new Error(`Unsupported language: ${language}`) - } - - // Add execution time to results - const executionTime = Math.round(performance.now() - startTime) - results.forEach(result => { - result.execution_time_ms = executionTime - }) - - - // Add to execution history - const success = !results.some(result => result.type === 'error') - if (artifactId) { - executionHistory.addExecution({ - artifactId, - code, - language: language.toLowerCase(), - results, - executionTime, - success, - tags: this.generateTags(code, language, results) - }) - } - - return results - } catch (error) { - const executionTime = Math.round(performance.now() - startTime) - const results = [{ - - id: Date.now().toString(), - type: 'error', - content: error instanceof Error ? error.message : String(error), - timestamp: new Date().toISOString(), - execution_time_ms: executionTime - - }] as ExecutionResult[] - - // Add failed execution to history - if (artifactId) { - executionHistory.addExecution({ - artifactId, - code, - language: language.toLowerCase(), - results, - executionTime, - success: false, - tags: this.generateTags(code, language, results) - }) - } - - return results - } - } - - - /** - * Generate tags for execution based on code analysis - */ - private generateTags(code: string, language: string, results: ExecutionResult[]): string[] { - const tags: string[] = [] - - // Language tag - tags.push(language.toLowerCase()) - - // Feature detection - const lowerCode = code.toLowerCase() - - // JavaScript/TypeScript specific - if (language === 'javascript' || language === 'js' || language === 'typescript' || language === 'ts') { - if (lowerCode.includes('async') || lowerCode.includes('await') || lowerCode.includes('promise')) { - tags.push('async') - } - if (lowerCode.includes('canvas') || lowerCode.includes('createcanvas')) { - tags.push('graphics') - } - if (lowerCode.includes('fetch') || lowerCode.includes('axios')) { - tags.push('network') - } - if (lowerCode.includes('class ') || lowerCode.includes('extends')) { - tags.push('oop') - } - if (lowerCode.includes('function') || lowerCode.includes('=>')) { - tags.push('functions') - } - if (lowerCode.includes('for') || lowerCode.includes('while') || lowerCode.includes('map') || lowerCode.includes('filter')) { - tags.push('loops') - } - if (lowerCode.includes('// @import')) { - tags.push('libraries') - } - } - - // Python specific - if (language === 'python' || language === 'py') { - if (lowerCode.includes('import numpy') || lowerCode.includes('import pandas')) { - tags.push('data-science') - } - if (lowerCode.includes('matplotlib') || lowerCode.includes('plt.')) { - tags.push('visualization') - } - if (lowerCode.includes('sklearn') || lowerCode.includes('scikit-learn')) { - tags.push('machine-learning') - } - if (lowerCode.includes('def ') || lowerCode.includes('lambda')) { - tags.push('functions') - } - if (lowerCode.includes('class ')) { - tags.push('oop') - } - if (lowerCode.includes('for ') || lowerCode.includes('while ')) { - tags.push('loops') - } - if (lowerCode.includes('requests') || lowerCode.includes('urllib')) { - tags.push('network') - } - } - - // Result-based tags - if (results.some(r => r.type === 'error')) { - tags.push('error') - } - if (results.some(r => r.type === 'canvas')) { - tags.push('canvas') - } - if (results.some(r => r.type === 'matplotlib')) { - tags.push('matplotlib') - } - if (results.length > 5) { - tags.push('verbose') - } - - // Performance tags - const totalTime = results.reduce((sum, r) => sum + (r.execution_time_ms || 0), 0) - if (totalTime > 5000) { - tags.push('slow') - } else if (totalTime < 100) { - tags.push('fast') - } - - return tags - - } - - /** - * Check if a language is supported for execution - */ - isLanguageSupported(language: string): boolean { - - const supportedLanguages = ['javascript', 'js', 'typescript', 'ts', 'python', 'py'] - - return supportedLanguages.includes(language.toLowerCase()) - } - - /** - * Check if a code artifact is executable - */ - isExecutable(artifact: { type: string; language?: string }): boolean { - - if (artifact.type !== 'code' && artifact.type !== 'executable-code') return false - - if (!artifact.language) return false - return this.isLanguageSupported(artifact.language) - } - - /** - * Load a JavaScript library - */ - async loadLibrary(libraryName: string): Promise { - if (!this.jsWorker) { - throw new Error('JavaScript worker not available') - } - - const requestId = this.generateRequestId() - - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - this.pendingRequests.delete(requestId) - reject(new Error('Library loading timed out')) - }, 30000) // 30 second timeout for library loading - - this.pendingRequests.set(requestId, { - resolve: (data: any) => resolve(data.success), - reject, - timeout - }) - - this.jsWorker!.postMessage({ - type: 'loadLibrary', - libraryName, - requestId - }) - }) - } - - /** - * Get available libraries - */ - async getAvailableLibraries(): Promise { - if (!this.jsWorker) { - throw new Error('JavaScript worker not available') - } - - const requestId = this.generateRequestId() - - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - this.pendingRequests.delete(requestId) - reject(new Error('Getting libraries timed out')) - }, 5000) - - this.pendingRequests.set(requestId, { - resolve, - reject, - timeout - }) - - this.jsWorker!.postMessage({ - type: 'getAvailableLibraries', - requestId - }) - }) - } - - /** - * Load a Python package - */ - async loadPythonPackage(packageName: string): Promise { - if (!this.pyWorker) { - throw new Error('Python worker not available') - } - - const requestId = this.generateRequestId() - - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - this.pendingRequests.delete(requestId) - reject(new Error('Package loading timed out')) - }, 60000) // 60 second timeout for Python package loading - - this.pendingRequests.set(requestId, { - resolve: (data: any) => resolve(data.success), - reject, - timeout - }) - - this.pyWorker!.postMessage({ - type: 'loadPackage', - packageName, - requestId - }) - }) - } - - /** - * Get available Python packages - */ - async getAvailablePythonPackages(): Promise { - if (!this.pyWorker) { - throw new Error('Python worker not available') - } - - const requestId = this.generateRequestId() - - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - this.pendingRequests.delete(requestId) - reject(new Error('Getting packages timed out')) - }, 5000) - - this.pendingRequests.set(requestId, { - resolve, - reject, - timeout - }) - - this.pyWorker!.postMessage({ - type: 'getAvailablePackages', - requestId - }) - }) - } - - /** - * Initialize Python environment - */ - async initializePython(): Promise { - if (!this.pyWorker) { - throw new Error('Python worker not available') - } - - const requestId = this.generateRequestId() - - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - this.pendingRequests.delete(requestId) - reject(new Error('Python initialization timed out')) - }, 30000) // 30 second timeout for initialization - - this.pendingRequests.set(requestId, { - resolve: (data: any) => resolve(data.success), - reject, - timeout - }) - - this.pyWorker!.postMessage({ - type: 'initialize', - requestId - }) - }) - } - - /** - - * Check if canvas output is supported - */ - isCanvasSupported(): boolean { - return true - } - - /** - * Render canvas operations to actual canvas element - */ - renderCanvasToElement(canvasData: string, canvasElement: HTMLCanvasElement): boolean { - try { - const data = JSON.parse(canvasData) as CanvasOperation - if (data.type !== 'canvas') return false - - const ctx = canvasElement.getContext('2d') - if (!ctx) return false - - // Set canvas size - canvasElement.width = data.width - canvasElement.height = data.height - - // Clear canvas - ctx.clearRect(0, 0, data.width, data.height) - - // Execute operations - for (const op of data.operations) { - switch (op.type) { - case 'fillRect': - if (op.style) ctx.fillStyle = op.style - ctx.fillRect(op.x, op.y, op.width, op.height) - break - case 'strokeRect': - if (op.style) ctx.strokeStyle = op.style - if (op.lineWidth) ctx.lineWidth = op.lineWidth - ctx.strokeRect(op.x, op.y, op.width, op.height) - break - case 'beginPath': - ctx.beginPath() - break - case 'closePath': - ctx.closePath() - break - case 'moveTo': - ctx.moveTo(op.x, op.y) - break - case 'lineTo': - ctx.lineTo(op.x, op.y) - break - case 'arc': - ctx.arc(op.x, op.y, op.radius, op.startAngle, op.endAngle) - break - case 'fill': - if (op.style) ctx.fillStyle = op.style - ctx.fill() - break - case 'stroke': - if (op.style) ctx.strokeStyle = op.style - if (op.lineWidth) ctx.lineWidth = op.lineWidth - ctx.stroke() - break - case 'clearRect': - ctx.clearRect(op.x, op.y, op.width, op.height) - break - case 'fillText': - if (op.style) ctx.fillStyle = op.style - ctx.fillText(op.text, op.x, op.y) - break - case 'strokeText': - if (op.style) ctx.strokeStyle = op.style - ctx.strokeText(op.text, op.x, op.y) - break - } - } - - return true - } catch (error) { - console.error('Failed to render canvas:', error) - return false - } - } - - /** - - * Render matplotlib plot to image element - */ - renderMatplotlibToElement(plotData: string, imgElement: HTMLImageElement): boolean { - try { - const data = JSON.parse(plotData) - if (data.type !== 'matplotlib') return false - - imgElement.src = `data:image/png;base64,${data.data}` - return true - } catch (error) { - console.error('Failed to render matplotlib plot:', error) - return false - } - } - - /** - * Get execution capabilities info - */ - getCapabilities() { - return { - javascript: { - supported: true, - features: [ - 'console output', - 'return values', - 'error handling', - 'timeouts', - 'library loading', - 'canvas graphics', - 'enhanced APIs' - ], - limitations: ['no DOM access', 'no direct network requests from user code', 'VFS-only file system'], - libraries: [ - 'lodash', 'd3', 'chart.js', 'moment', 'axios', 'rxjs', 'p5', 'three', 'fabric' - ] - }, - python: { - - supported: true, - features: [ - 'print output', - 'matplotlib plots', - 'scientific computing', - 'data analysis', - 'package loading', - 'error handling', - 'timeouts', - 'memory monitoring' - ], - limitations: ['VFS-only file system', 'no direct network requests', 'limited to Pyodide packages'], - packages: [ - 'numpy', 'pandas', 'matplotlib', 'scipy', 'scikit-learn', 'requests', - 'beautifulsoup4', 'pillow', 'sympy', 'networkx', 'seaborn', 'plotly', 'bokeh', 'altair' - ] - - } - } - } - - /** - * Dispose of resources - */ - dispose() { - // Clear all pending requests - for (const [requestId, request] of this.pendingRequests) { - if (request.timeout) { - clearTimeout(request.timeout) - } - request.reject(new Error('CodeRunner disposed')) - } - this.pendingRequests.clear() - - - // Terminate workers - - if (this.jsWorker) { - this.jsWorker.terminate() - this.jsWorker = null - } - - if (this.pyWorker) { - this.pyWorker.terminate() - this.pyWorker = null - } - - } -} - -// Global instance for sharing across components -let globalCodeRunner: CodeRunner | null = null - -export function getCodeRunner(): CodeRunner { - if (!globalCodeRunner) { - globalCodeRunner = new CodeRunner() - } - return globalCodeRunner -} - -export function disposeCodeRunner() { - if (globalCodeRunner) { - globalCodeRunner.dispose() - globalCodeRunner = null - } -} diff --git a/web/src/services/executionHistory.ts b/web/src/services/executionHistory.ts deleted file mode 100644 index 8eb9700d..00000000 --- a/web/src/services/executionHistory.ts +++ /dev/null @@ -1,424 +0,0 @@ -/** - * Execution History Service - * Manages execution history, persistence, and analytics - */ - -import { reactive, computed } from 'vue' -import type { ExecutionResult } from './codeRunner' - -export interface ExecutionHistoryEntry { - id: string - artifactId: string - timestamp: string - code: string - language: string - results: ExecutionResult[] - executionTime: number - success: boolean - tags: string[] - notes?: string -} - -export interface ExecutionStats { - totalExecutions: number - totalTime: number - successRate: number - languageBreakdown: Record - averageExecutionTime: number - recentActivity: ExecutionHistoryEntry[] -} - -class ExecutionHistoryService { - private static instance: ExecutionHistoryService - private history: ExecutionHistoryEntry[] = reactive([]) - private maxHistorySize = 1000 - private storageKey = 'code-runner-history' - - private constructor() { - this.loadFromStorage() - } - - static getInstance(): ExecutionHistoryService { - if (!ExecutionHistoryService.instance) { - ExecutionHistoryService.instance = new ExecutionHistoryService() - } - return ExecutionHistoryService.instance - } - - /** - * Add an execution to history - */ - addExecution(entry: Omit): string { - const id = this.generateId() - const timestamp = new Date().toISOString() - - const historyEntry: ExecutionHistoryEntry = { - id, - timestamp, - ...entry - } - - this.history.unshift(historyEntry) - - // Keep history size manageable - if (this.history.length > this.maxHistorySize) { - this.history.splice(this.maxHistorySize) - } - - this.saveToStorage() - return id - } - - /** - * Get execution history - */ - getHistory(limit?: number): ExecutionHistoryEntry[] { - return limit ? this.history.slice(0, limit) : [...this.history] - } - - /** - * Get history for specific artifact - */ - getArtifactHistory(artifactId: string, limit?: number): ExecutionHistoryEntry[] { - const artifactHistory = this.history.filter(entry => entry.artifactId === artifactId) - return limit ? artifactHistory.slice(0, limit) : artifactHistory - } - - /** - * Get execution by ID - */ - getExecution(id: string): ExecutionHistoryEntry | undefined { - return this.history.find(entry => entry.id === id) - } - - /** - * Search history - */ - searchHistory(query: string, filters?: { - language?: string - success?: boolean - tags?: string[] - dateFrom?: Date - dateTo?: Date - }): ExecutionHistoryEntry[] { - let results = this.history - - // Text search - if (query.trim()) { - const searchTerm = query.toLowerCase() - results = results.filter(entry => - entry.code.toLowerCase().includes(searchTerm) || - entry.notes?.toLowerCase().includes(searchTerm) || - entry.tags.some(tag => tag.toLowerCase().includes(searchTerm)) - ) - } - - // Apply filters - if (filters) { - if (filters.language) { - results = results.filter(entry => entry.language === filters.language) - } - - if (filters.success !== undefined) { - results = results.filter(entry => entry.success === filters.success) - } - - if (filters.tags && filters.tags.length > 0) { - results = results.filter(entry => - filters.tags!.some(tag => entry.tags.includes(tag)) - ) - } - - if (filters.dateFrom) { - results = results.filter(entry => - new Date(entry.timestamp) >= filters.dateFrom! - ) - } - - if (filters.dateTo) { - results = results.filter(entry => - new Date(entry.timestamp) <= filters.dateTo! - ) - } - } - - return results - } - - /** - * Get execution statistics - */ - getStats(): ExecutionStats { - const totalExecutions = this.history.length - const totalTime = this.history.reduce((sum, entry) => sum + entry.executionTime, 0) - const successfulExecutions = this.history.filter(entry => entry.success).length - const successRate = totalExecutions > 0 ? successfulExecutions / totalExecutions : 0 - - // Language breakdown - const languageBreakdown: Record = {} - this.history.forEach(entry => { - languageBreakdown[entry.language] = (languageBreakdown[entry.language] || 0) + 1 - }) - - const averageExecutionTime = totalExecutions > 0 ? totalTime / totalExecutions : 0 - - // Recent activity (last 10 executions) - const recentActivity = this.history.slice(0, 10) - - return { - totalExecutions, - totalTime, - successRate, - languageBreakdown, - averageExecutionTime, - recentActivity - } - } - - /** - * Update execution notes - */ - updateNotes(id: string, notes: string): boolean { - const entry = this.history.find(entry => entry.id === id) - if (entry) { - entry.notes = notes - this.saveToStorage() - return true - } - return false - } - - /** - * Add tags to execution - */ - addTags(id: string, tags: string[]): boolean { - const entry = this.history.find(entry => entry.id === id) - if (entry) { - const newTags = tags.filter(tag => !entry.tags.includes(tag)) - entry.tags.push(...newTags) - this.saveToStorage() - return true - } - return false - } - - /** - * Remove tags from execution - */ - removeTags(id: string, tags: string[]): boolean { - const entry = this.history.find(entry => entry.id === id) - if (entry) { - entry.tags = entry.tags.filter(tag => !tags.includes(tag)) - this.saveToStorage() - return true - } - return false - } - - /** - * Delete execution from history - */ - deleteExecution(id: string): boolean { - const index = this.history.findIndex(entry => entry.id === id) - if (index >= 0) { - this.history.splice(index, 1) - this.saveToStorage() - return true - } - return false - } - - /** - * Clear all history - */ - clearHistory(): void { - this.history.splice(0) - this.saveToStorage() - } - - /** - * Export history as JSON - */ - exportHistory(): string { - return JSON.stringify(this.history, null, 2) - } - - /** - * Import history from JSON - */ - importHistory(jsonData: string): boolean { - try { - const importedHistory = JSON.parse(jsonData) as ExecutionHistoryEntry[] - - // Validate imported data - if (!Array.isArray(importedHistory)) { - throw new Error('Invalid format: expected array') - } - - // Validate each entry - importedHistory.forEach((entry, index) => { - if (!entry.id || !entry.timestamp || !entry.code || !entry.language) { - throw new Error(`Invalid entry at index ${index}: missing required fields`) - } - }) - - // Merge with existing history, avoiding duplicates - const existingIds = new Set(this.history.map(entry => entry.id)) - const newEntries = importedHistory.filter(entry => !existingIds.has(entry.id)) - - this.history.unshift(...newEntries) - - // Sort by timestamp - this.history.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) - - // Trim to max size - if (this.history.length > this.maxHistorySize) { - this.history.splice(this.maxHistorySize) - } - - this.saveToStorage() - return true - } catch (error) { - console.error('Failed to import history:', error) - return false - } - } - - /** - * Get popular code snippets - */ - getPopularSnippets(language?: string, limit = 10): Array<{ - code: string - language: string - count: number - lastUsed: string - }> { - let entries = this.history - - if (language) { - entries = entries.filter(entry => entry.language === language) - } - - // Group by similar code (first 100 characters) - const codeGroups: Record = {} - - entries.forEach(entry => { - const key = entry.code.substring(0, 100).trim() - if (key.length > 10) { // Ignore very short snippets - if (!codeGroups[key]) { - codeGroups[key] = { - code: entry.code, - language: entry.language, - count: 0, - lastUsed: entry.timestamp - } - } - codeGroups[key].count++ - if (entry.timestamp > codeGroups[key].lastUsed) { - codeGroups[key].lastUsed = entry.timestamp - } - } - }) - - return Object.values(codeGroups) - .sort((a, b) => b.count - a.count) - .slice(0, limit) - } - - /** - * Get performance trends - */ - getPerformanceTrends(days = 30): Array<{ - date: string - executions: number - averageTime: number - successRate: number - }> { - const cutoffDate = new Date() - cutoffDate.setDate(cutoffDate.getDate() - days) - - const recentEntries = this.history.filter(entry => - new Date(entry.timestamp) >= cutoffDate - ) - - // Group by date - const dailyData: Record = {} - - recentEntries.forEach(entry => { - const date = new Date(entry.timestamp).toISOString().split('T')[0] - if (!dailyData[date]) { - dailyData[date] = { executions: 0, totalTime: 0, successes: 0 } - } - dailyData[date].executions++ - dailyData[date].totalTime += entry.executionTime - if (entry.success) { - dailyData[date].successes++ - } - }) - - return Object.entries(dailyData) - .map(([date, data]) => ({ - date, - executions: data.executions, - averageTime: data.totalTime / data.executions, - successRate: data.successes / data.executions - })) - .sort((a, b) => a.date.localeCompare(b.date)) - } - - private generateId(): string { - return `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` - } - - private saveToStorage(): void { - try { - localStorage.setItem(this.storageKey, JSON.stringify(this.history)) - } catch (error) { - console.warn('Failed to save execution history to localStorage:', error) - } - } - - private loadFromStorage(): void { - try { - const stored = localStorage.getItem(this.storageKey) - if (stored) { - const parsedHistory = JSON.parse(stored) as ExecutionHistoryEntry[] - this.history.splice(0, this.history.length, ...parsedHistory) - } - } catch (error) { - console.warn('Failed to load execution history from localStorage:', error) - } - } -} - -// Export singleton instance -export const executionHistory = ExecutionHistoryService.getInstance() - -// Export composable for Vue components -export function useExecutionHistory() { - return { - history: computed(() => executionHistory.getHistory()), - stats: computed(() => executionHistory.getStats()), - addExecution: executionHistory.addExecution.bind(executionHistory), - getArtifactHistory: executionHistory.getArtifactHistory.bind(executionHistory), - searchHistory: executionHistory.searchHistory.bind(executionHistory), - updateNotes: executionHistory.updateNotes.bind(executionHistory), - addTags: executionHistory.addTags.bind(executionHistory), - removeTags: executionHistory.removeTags.bind(executionHistory), - deleteExecution: executionHistory.deleteExecution.bind(executionHistory), - clearHistory: executionHistory.clearHistory.bind(executionHistory), - exportHistory: executionHistory.exportHistory.bind(executionHistory), - importHistory: executionHistory.importHistory.bind(executionHistory), - getPopularSnippets: executionHistory.getPopularSnippets.bind(executionHistory), - getPerformanceTrends: executionHistory.getPerformanceTrends.bind(executionHistory) - } -} \ No newline at end of file diff --git a/web/src/services/exportService.ts b/web/src/services/exportService.ts deleted file mode 100644 index b8968f04..00000000 --- a/web/src/services/exportService.ts +++ /dev/null @@ -1,828 +0,0 @@ -/** - * Export Service - * Handles exporting artifacts, code, and execution results in various formats - */ - -import JSZip from 'jszip' -import { saveAs } from 'file-saver' -import type { ExecutionResult } from './codeRunner' -import type { ExecutionHistoryEntry } from './executionHistory' -import type { CodeTemplate } from './codeTemplates' - -export interface ExportOptions { - format: 'single' | 'zip' | 'json' | 'html' | 'pdf' - includeHistory?: boolean - includeResults?: boolean - includeMetadata?: boolean - includeTimestamps?: boolean - template?: 'minimal' | 'detailed' | 'presentation' -} - -export interface ShareOptions { - title?: string - description?: string - language?: string - tags?: string[] - public?: boolean - expiration?: Date -} - -export interface ExportableArtifact { - id: string - title: string - content: string - type: string - language?: string - createdAt: string - updatedAt?: string - tags?: string[] - results?: ExecutionResult[] - history?: ExecutionHistoryEntry[] - metadata?: Record -} - -class ExportService { - private static instance: ExportService - - private constructor() {} - - static getInstance(): ExportService { - if (!ExportService.instance) { - ExportService.instance = new ExportService() - } - return ExportService.instance - } - - /** - * Export a single artifact - */ - async exportArtifact(artifact: ExportableArtifact, options: ExportOptions = { format: 'single' }): Promise { - switch (options.format) { - case 'single': - this.exportSingleFile(artifact, options) - break - case 'zip': - await this.exportAsZip([artifact], options) - break - case 'json': - this.exportAsJson([artifact], options) - break - case 'html': - this.exportAsHtml([artifact], options) - break - case 'pdf': - await this.exportAsPdf([artifact], options) - break - } - } - - /** - * Export multiple artifacts - */ - async exportArtifacts(artifacts: ExportableArtifact[], options: ExportOptions = { format: 'zip' }): Promise { - switch (options.format) { - case 'zip': - await this.exportAsZip(artifacts, options) - break - case 'json': - this.exportAsJson(artifacts, options) - break - case 'html': - this.exportAsHtml(artifacts, options) - break - case 'pdf': - await this.exportAsPdf(artifacts, options) - break - default: - throw new Error('Multiple artifacts require zip, json, html, or pdf format') - } - } - - /** - * Export execution history - */ - async exportHistory(history: ExecutionHistoryEntry[], options: ExportOptions = { format: 'json' }): Promise { - const exportData = { - type: 'execution_history', - exportedAt: new Date().toISOString(), - count: history.length, - history: history.map(entry => ({ - id: entry.id, - timestamp: entry.timestamp, - code: entry.code, - language: entry.language, - success: entry.success, - executionTime: entry.executionTime, - tags: entry.tags, - notes: entry.notes, - ...(options.includeResults && { results: entry.results }) - })) - } - - switch (options.format) { - case 'json': - this.downloadJson(exportData, 'execution_history.json') - break - case 'html': - this.exportHistoryAsHtml(exportData, options) - break - case 'zip': - await this.exportHistoryAsZip(exportData, options) - break - default: - throw new Error('Unsupported format for history export') - } - } - - /** - * Export code templates - */ - async exportTemplates(templates: CodeTemplate[], options: ExportOptions = { format: 'json' }): Promise { - const exportData = { - type: 'code_templates', - exportedAt: new Date().toISOString(), - count: templates.length, - templates: templates.map(template => ({ - ...template, - ...(options.includeMetadata && { - metadata: { - usageCount: template.usageCount, - rating: template.rating, - isBuiltIn: template.isBuiltIn - } - }) - })) - } - - switch (options.format) { - case 'json': - this.downloadJson(exportData, 'code_templates.json') - break - case 'zip': - await this.exportTemplatesAsZip(exportData, options) - break - default: - throw new Error('Unsupported format for template export') - } - } - - /** - * Share artifact via URL - */ - async shareArtifact(artifact: ExportableArtifact, options: ShareOptions = {}): Promise { - const shareData = { - title: options.title || artifact.title, - description: options.description || `Shared artifact: ${artifact.title}`, - content: artifact.content, - language: options.language || artifact.language, - type: artifact.type, - tags: options.tags || artifact.tags, - createdAt: new Date().toISOString(), - public: options.public ?? true, - expiration: options.expiration?.toISOString() - } - - // In a real implementation, this would POST to a sharing service - // For now, we'll create a data URL - const dataUrl = this.createDataUrl(shareData) - - // Copy to clipboard - try { - await navigator.clipboard.writeText(dataUrl) - return dataUrl - } catch (error) { - console.error('Failed to copy share URL:', error) - throw new Error('Failed to create share URL') - } - } - - /** - * Export project (multiple files) - */ - async exportProject(artifacts: ExportableArtifact[], projectName: string, options: ExportOptions = { format: 'zip' }): Promise { - const zip = new JSZip() - const projectFolder = zip.folder(projectName) - - if (!projectFolder) { - throw new Error('Failed to create project folder') - } - - // Add README - const readme = this.generateReadme(artifacts, projectName) - projectFolder.file('README.md', readme) - - // Add package.json for JavaScript projects - const hasJavaScript = artifacts.some(a => a.language === 'javascript' || a.language === 'typescript') - if (hasJavaScript) { - const packageJson = this.generatePackageJson(artifacts, projectName) - projectFolder.file('package.json', JSON.stringify(packageJson, null, 2)) - } - - // Add requirements.txt for Python projects - const hasPython = artifacts.some(a => a.language === 'python') - if (hasPython) { - const requirements = this.generateRequirements(artifacts) - projectFolder.file('requirements.txt', requirements) - } - - // Add artifacts - artifacts.forEach(artifact => { - const filename = this.generateFileName(artifact) - const folder = this.getArtifactFolder(artifact, projectFolder) - folder.file(filename, artifact.content) - - // Add metadata if requested - if (options.includeMetadata) { - const metadata = this.generateMetadata(artifact, options) - folder.file(`${filename}.meta.json`, JSON.stringify(metadata, null, 2)) - } - - // Add execution results if requested - if (options.includeResults && artifact.results) { - const resultsFile = this.generateResultsFile(artifact.results) - folder.file(`${filename}.results.json`, resultsFile) - } - }) - - // Generate and download zip - const content = await zip.generateAsync({ type: 'blob' }) - saveAs(content, `${projectName}.zip`) - } - - /** - * Export as single file - */ - private exportSingleFile(artifact: ExportableArtifact, options: ExportOptions): void { - const filename = this.generateFileName(artifact) - const content = this.prepareContent(artifact, options) - - const blob = new Blob([content], { type: 'text/plain' }) - saveAs(blob, filename) - } - - /** - * Export as ZIP - */ - private async exportAsZip(artifacts: ExportableArtifact[], options: ExportOptions): Promise { - const zip = new JSZip() - const timestamp = new Date().toISOString().split('T')[0] - - artifacts.forEach((artifact, index) => { - const filename = this.generateFileName(artifact, index) - const content = this.prepareContent(artifact, options) - zip.file(filename, content) - - if (options.includeMetadata) { - const metadata = this.generateMetadata(artifact, options) - zip.file(`${filename}.meta.json`, JSON.stringify(metadata, null, 2)) - } - }) - - // Add manifest - const manifest = this.generateManifest(artifacts, options) - zip.file('manifest.json', JSON.stringify(manifest, null, 2)) - - const content = await zip.generateAsync({ type: 'blob' }) - saveAs(content, `artifacts_${timestamp}.zip`) - } - - /** - * Export as JSON - */ - private exportAsJson(artifacts: ExportableArtifact[], options: ExportOptions): void { - const exportData = { - type: 'artifacts', - exportedAt: new Date().toISOString(), - count: artifacts.length, - options, - artifacts: artifacts.map(artifact => ({ - ...artifact, - ...(options.includeHistory && { history: artifact.history }), - ...(options.includeResults && { results: artifact.results }), - ...(options.includeMetadata && { metadata: artifact.metadata }) - })) - } - - this.downloadJson(exportData, `artifacts_${new Date().toISOString().split('T')[0]}.json`) - } - - /** - * Export as HTML - */ - private exportAsHtml(artifacts: ExportableArtifact[], options: ExportOptions): void { - const html = this.generateHtmlReport(artifacts, options) - const blob = new Blob([html], { type: 'text/html' }) - saveAs(blob, `artifacts_${new Date().toISOString().split('T')[0]}.html`) - } - - /** - * Export as PDF - */ - private async exportAsPdf(artifacts: ExportableArtifact[], options: ExportOptions): Promise { - // This would require a PDF library like jsPDF - // For now, we'll export as HTML with print-friendly styling - const html = this.generatePrintableHtml(artifacts, options) - const blob = new Blob([html], { type: 'text/html' }) - saveAs(blob, `artifacts_${new Date().toISOString().split('T')[0]}_printable.html`) - } - - /** - * Generate HTML report - */ - private generateHtmlReport(artifacts: ExportableArtifact[], options: ExportOptions): string { - const timestamp = new Date().toISOString() - const template = options.template || 'detailed' - - return ` - - - - - Artifact Export Report - - - -
-

Artifact Export Report

-

Generated on ${new Date(timestamp).toLocaleString()}

-

Total artifacts: ${artifacts.length}

-
- - ${artifacts.map(artifact => ` -
-
-
${artifact.title}
-
- Type: ${artifact.type} - ${artifact.language ? `Language: ${artifact.language}` : ''} - Created: ${new Date(artifact.createdAt).toLocaleDateString()} -
-
- -
-
${this.escapeHtml(artifact.content)}
- - ${artifact.tags && artifact.tags.length > 0 ? ` -
- Tags: - ${artifact.tags.map(tag => `${tag}`).join('')} -
- ` : ''} - - ${options.includeResults && artifact.results ? ` -
- Execution Results: -
${JSON.stringify(artifact.results, null, 2)}
-
- ` : ''} -
-
- `).join('')} - - - -` - } - - /** - * Generate printable HTML - */ - private generatePrintableHtml(artifacts: ExportableArtifact[], options: ExportOptions): string { - const html = this.generateHtmlReport(artifacts, options) - // Add print-specific styles - return html.replace(' - - -

Execution History

-

Exported: ${exportData.exportedAt}

-

Total entries: ${exportData.count}

- - ${exportData.history.map((entry: any, index: number) => ` -
-

Execution ${index + 1}

-

Date: ${new Date(entry.timestamp).toLocaleString()}

-

Language: ${entry.language}

-

Success: ${entry.success ? 'Yes' : 'No'}

-

Execution Time: ${entry.executionTime}ms

-
${this.escapeHtml(entry.code)}
-
- `).join('')} - -` - - const blob = new Blob([html], { type: 'text/html' }) - saveAs(blob, `execution_history_${new Date().toISOString().split('T')[0]}.html`) - } - - private async exportTemplatesAsZip(exportData: any, options: ExportOptions): Promise { - const zip = new JSZip() - - // Add main templates file - zip.file('templates.json', JSON.stringify(exportData, null, 2)) - - // Add individual template files - exportData.templates.forEach((template: any) => { - const filename = this.generateFileName(template) - zip.file(filename, template.code) - }) - - const content = await zip.generateAsync({ type: 'blob' }) - saveAs(content, `code_templates_${new Date().toISOString().split('T')[0]}.zip`) - } -} - -// Export singleton instance -export const exportService = ExportService.getInstance() - -// Export composable for Vue components -export function useExportService() { - return { - exportArtifact: exportService.exportArtifact.bind(exportService), - exportArtifacts: exportService.exportArtifacts.bind(exportService), - exportHistory: exportService.exportHistory.bind(exportService), - exportTemplates: exportService.exportTemplates.bind(exportService), - exportProject: exportService.exportProject.bind(exportService), - shareArtifact: exportService.shareArtifact.bind(exportService) - } -} \ No newline at end of file diff --git a/web/src/typings/chat.d.ts b/web/src/typings/chat.d.ts index 7333a9ff..0c95b36c 100644 --- a/web/src/typings/chat.d.ts +++ b/web/src/typings/chat.d.ts @@ -2,20 +2,10 @@ declare namespace Chat { interface Artifact { uuid: string - type: string // 'code', 'html', 'svg', 'mermaid', 'json', 'markdown', 'executable-code' + type: string // 'code', 'html', 'svg', 'mermaid', 'json', 'markdown' title: string content: string language?: string // for code artifacts - isExecutable?: boolean // for executable code artifacts - executionResults?: ExecutionResult[] - } - - interface ExecutionResult { - id: string - type: 'log' | 'error' | 'return' | 'stdout' | 'warn' | 'info' | 'debug' | 'canvas' | 'matplotlib' - content: string - timestamp: string - execution_time_ms?: number } interface Message { @@ -48,10 +38,8 @@ declare namespace Chat { maxTokens?: number debug?: boolean summarizeMode?: boolean - codeRunnerEnabled?: boolean exploreMode?: boolean artifactEnabled?: boolean - showToolDebug?: boolean workspaceUuid?: string } diff --git a/web/src/utils/artifacts.ts b/web/src/utils/artifacts.ts index d7c7b27c..d436682c 100644 --- a/web/src/utils/artifacts.ts +++ b/web/src/utils/artifacts.ts @@ -8,50 +8,6 @@ function generateUUID(): string { return uuid() } -// Check if a language is supported for code execution -function isExecutableLanguage(language: string): boolean { - const executableLanguages = [ - 'javascript', 'js', 'typescript', 'ts', - 'python', 'py' - ] - - const normalizedLanguage = language.toLowerCase().trim() - return executableLanguages.includes(normalizedLanguage) -} - -// Check if code contains patterns that suggest it should be executable -function containsExecutablePatterns(content: string): boolean { - // Patterns that suggest the code is meant to be executed - const executablePatterns = [ - // JavaScript patterns - 'console.log', - 'console.error', - 'console.warn', - 'function', - 'const ', - 'let ', - 'var ', - '=>', - 'if (', - 'for (', - 'while (', - 'return ', - - // Python patterns - 'print(', - 'import ', - 'from ', - 'def ', - 'if __name__', - 'class ', - 'for ', - 'while ' - ] - - const contentLower = content.toLowerCase() - return executablePatterns.some(pattern => contentLower.includes(pattern)) -} - // Extract artifacts from message content (mirrors backend logic) export function extractArtifacts(content: string): Artifact[] { const artifacts: Artifact[] = [] @@ -128,8 +84,7 @@ export function extractArtifacts(content: string): Artifact[] { artifacts.push(artifact) } - // Pattern for executable code artifacts - // Example: ```javascript + // Backward-compatible parsing for legacy executable markers. const executableArtifactRegex = /```(\w+)?\s*\s*\n(.*?)\n```/gs const executableMatches = content.matchAll(executableArtifactRegex) @@ -143,17 +98,14 @@ export function extractArtifacts(content: string): Artifact[] { continue } - // Only create executable artifacts for supported languages - if (isExecutableLanguage(language)) { - const artifact: Artifact = { - uuid: generateUUID(), - type: 'executable-code', - title, - content: artifactContent, - language - } - artifacts.push(artifact) + const artifact: Artifact = { + uuid: generateUUID(), + type: 'code', + title, + content: artifactContent, + language } + artifacts.push(artifact) } // Pattern for general code artifacts (exclude html, svg, mermaid, json which are handled above) @@ -170,18 +122,9 @@ export function extractArtifacts(content: string): Artifact[] { continue } - // Check if this should be an executable artifact for supported languages - let artifactType = 'code' - if (isExecutableLanguage(language)) { - // For supported languages, make them executable by default if they contain certain patterns - if (containsExecutablePatterns(artifactContent)) { - artifactType = 'executable-code' - } - } - const artifact: Artifact = { uuid: generateUUID(), - type: artifactType, + type: 'code', title, content: artifactContent, language @@ -190,4 +133,4 @@ export function extractArtifacts(content: string): Artifact[] { } return artifacts -} \ No newline at end of file +} diff --git a/web/src/utils/vfs-examples.js b/web/src/utils/vfs-examples.js deleted file mode 100644 index 0c14f029..00000000 --- a/web/src/utils/vfs-examples.js +++ /dev/null @@ -1,610 +0,0 @@ -/** - * VFS Import/Export Examples - * Demonstrates all import/export capabilities of the Virtual File System - */ - -// Example usage of VFS Import/Export system -const vfsImportExportExamples = { - - // Basic file upload and download - fileOperations: { - title: "Basic File Upload/Download", - description: "Upload files from your computer and download files from VFS", - javascript: ` -// Initialize VFS and Import/Export -const vfs = new VirtualFileSystem(); -const importExport = new VFSImportExport(vfs); - -// Simulate file upload (in real usage, this would be from a file input) -const fileContent = "Hello, VFS!\\nThis is uploaded content."; -const mockFile = new Blob([fileContent], { type: 'text/plain' }); -mockFile.name = 'uploaded.txt'; - -// Upload file to VFS -const uploadResult = await importExport.uploadFile(mockFile, '/data/uploaded.txt'); -console.log('Upload result:', uploadResult); - -// Verify file exists in VFS -console.log('File exists:', vfs.exists('/data/uploaded.txt')); - -// Read file content from VFS -const content = await vfs.readFile('/data/uploaded.txt', 'utf8'); -console.log('File content:', content); - -// Download file (in real usage, this would trigger browser download) -const downloadResult = await importExport.downloadFile('/data/uploaded.txt'); -console.log('Download result:', downloadResult); -`, - python: ` -# In Python, files uploaded via the UI are automatically available -import os - -# Check if uploaded file exists -if os.path.exists('/data/uploaded.txt'): - print("Uploaded file is available in Python!") - - # Read the uploaded content - with open('/data/uploaded.txt', 'r') as f: - content = f.read() - print(f"Content: {content}") - - # Process and save new file - processed_content = content.upper() - with open('/data/processed.txt', 'w') as f: - f.write(processed_content) - - print("Processed file saved to /data/processed.txt") -` - }, - - // Data format conversion - dataConversion: { - title: "Data Format Conversion", - description: "Convert between CSV, JSON, XML formats", - javascript: ` -const fs = require('fs'); - -// Create sample CSV data -const csvData = \`name,age,city,salary -John Doe,30,New York,75000 -Jane Smith,25,San Francisco,85000 -Bob Johnson,35,Chicago,65000 -Alice Brown,28,Boston,70000\`; - -// Write CSV file -fs.writeFileSync('/data/employees.csv', csvData); - -// Convert CSV to JSON using import/export system -const convertResult = await importExport.convertToFormat('/data/employees.csv', 'json', '/data/employees.json'); -console.log('Conversion result:', convertResult); - -// Read and display JSON data -const jsonData = fs.readFileSync('/data/employees.json', 'utf8'); -console.log('JSON data:', JSON.parse(jsonData)); - -// Convert back to CSV with different name -await importExport.convertToFormat('/data/employees.json', 'csv', '/data/employees_converted.csv'); - -// Verify the round-trip conversion -const convertedCsv = fs.readFileSync('/data/employees_converted.csv', 'utf8'); -console.log('Converted back to CSV:', convertedCsv); -`, - python: ` -import json -import csv -import pandas as pd - -# Read the JSON file created by JavaScript -with open('/data/employees.json', 'r') as f: - employees = json.load(f) - -print("Employee data from JSON:") -for employee in employees: - print(f" {employee['name']}: {employee['salary']}") - -# Use pandas for advanced processing -df = pd.read_csv('/data/employees.csv') - -# Calculate statistics -avg_salary = df['salary'].mean() -max_salary = df['salary'].max() -min_salary = df['salary'].min() - -# Create summary report -summary = { - "total_employees": len(df), - "average_salary": avg_salary, - "max_salary": max_salary, - "min_salary": min_salary, - "cities": df['city'].unique().tolist(), - "avg_age": df['age'].mean() -} - -# Save summary as JSON -with open('/data/salary_summary.json', 'w') as f: - json.dump(summary, f, indent=2) - -print("Summary report saved to /data/salary_summary.json") - -# Export high earners to separate CSV -high_earners = df[df['salary'] > 70000] -high_earners.to_csv('/data/high_earners.csv', index=False) - -print(f"Found {len(high_earners)} high earners") -` - }, - - // Import from URL - urlImport: { - title: "Import Data from URL", - description: "Fetch data from external URLs", - javascript: ` -// Import CSV data from a public dataset URL -const publicDataUrl = 'https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/7_OneCatOneNum_header.csv'; - -try { - const importResult = await importExport.importFromURL(publicDataUrl, '/data/public_dataset.csv'); - - if (importResult.success) { - console.log('Import successful:', importResult); - - // Process the imported data - const csvContent = fs.readFileSync('/data/public_dataset.csv', 'utf8'); - const lines = csvContent.split('\\n'); - console.log(\`Imported \${lines.length - 1} data rows\`); - console.log('First few lines:', lines.slice(0, 5)); - - // Convert to JSON for easier processing - await importExport.convertToFormat('/data/public_dataset.csv', 'json', '/data/public_dataset.json'); - - } else { - console.error('Import failed:', importResult.message); - } -} catch (error) { - console.error('URL import error:', error.message); - - // Fallback: Create sample data - console.log('Creating sample data instead...'); - const sampleData = \`group,value -A,23 -B,45 -C,56 -D,78 -E,32\`; - - fs.writeFileSync('/data/sample_dataset.csv', sampleData); - console.log('Sample data created at /data/sample_dataset.csv'); -} -`, - python: ` -import pandas as pd -import json - -# Check if the imported data exists -import os -if os.path.exists('/data/public_dataset.csv'): - print("Processing imported public dataset...") - df = pd.read_csv('/data/public_dataset.csv') - - # Analyze the data - print(f"Dataset shape: {df.shape}") - print("\\nColumn info:") - print(df.info()) - - print("\\nFirst few rows:") - print(df.head()) - - # Generate statistics - stats = df.describe() - print("\\nStatistics:") - print(stats) - - # Save analysis results - analysis = { - "shape": df.shape, - "columns": df.columns.tolist(), - "dtypes": df.dtypes.astype(str).to_dict(), - "null_counts": df.isnull().sum().to_dict(), - "summary_stats": stats.to_dict() if len(df.select_dtypes(include='number').columns) > 0 else {} - } - - with open('/data/dataset_analysis.json', 'w') as f: - json.dump(analysis, f, indent=2) - - print("Analysis saved to /data/dataset_analysis.json") - -elif os.path.exists('/data/sample_dataset.csv'): - print("Processing sample dataset...") - df = pd.read_csv('/data/sample_dataset.csv') - - # Simple analysis of sample data - print("Sample data overview:") - print(df) - - # Calculate total and mean - total_value = df['value'].sum() - mean_value = df['value'].mean() - - print(f"\\nTotal value: {total_value}") - print(f"Mean value: {mean_value:.2f}") - - # Create visualization data - viz_data = df.to_dict('records') - with open('/data/visualization_data.json', 'w') as f: - json.dump(viz_data, f, indent=2) - - print("Visualization data saved to /data/visualization_data.json") -else: - print("No dataset found. Please run the JavaScript import first.") -` - }, - - // Session management - sessionManagement: { - title: "Session Import/Export", - description: "Save and restore entire VFS sessions", - javascript: ` -// Create a complex file structure for demo -const fs = require('fs'); - -// Setup project structure -fs.mkdirSync('/workspace/myproject', { recursive: true }); -fs.mkdirSync('/workspace/myproject/src', { recursive: true }); -fs.mkdirSync('/workspace/myproject/data', { recursive: true }); -fs.mkdirSync('/workspace/myproject/docs', { recursive: true }); - -// Create project files -const packageJson = { - name: "vfs-demo-project", - version: "1.0.0", - description: "A demo project using VFS", - main: "src/index.js", - scripts: { - start: "node src/index.js", - test: "echo \\"No tests yet\\"" - } -}; - -fs.writeFileSync('/workspace/myproject/package.json', JSON.stringify(packageJson, null, 2)); - -const mainCode = \` -console.log('Welcome to VFS Demo Project!'); - -const fs = require('fs'); -const path = require('path'); - -// Read project configuration -const packageInfo = JSON.parse(fs.readFileSync('./package.json', 'utf8')); -console.log(\`Project: \${packageInfo.name} v\${packageInfo.version}\`); - -// Read data files -const dataDir = './data'; -if (fs.existsSync(dataDir)) { - const dataFiles = fs.readdirSync(dataDir); - console.log(\`Found \${dataFiles.length} data files:\`, dataFiles); -} -\`; - -fs.writeFileSync('/workspace/myproject/src/index.js', mainCode); - -// Create README -const readme = \`# VFS Demo Project - -This is a demonstration project showing the capabilities of the Virtual File System. - -## Features - -- File management -- Data processing -- Session persistence - -## Usage - -\\\`\\\`\\\`bash -npm start -\\\`\\\`\\\` - -## Files - -- \`src/index.js\` - Main application -- \`data/\` - Data files -- \`docs/\` - Documentation -\`; - -fs.writeFileSync('/workspace/myproject/README.md', readme); - -// Create some data files -const sampleData = [ - { id: 1, name: 'Sample 1', value: 100 }, - { id: 2, name: 'Sample 2', value: 200 }, - { id: 3, name: 'Sample 3', value: 300 } -]; - -fs.writeFileSync('/workspace/myproject/data/samples.json', JSON.stringify(sampleData, null, 2)); - -// Create CSV data -const csvData = 'id,name,value\\n1,Sample 1,100\\n2,Sample 2,200\\n3,Sample 3,300'; -fs.writeFileSync('/workspace/myproject/data/samples.csv', csvData); - -console.log('Project structure created!'); - -// Export the entire VFS session -const exportResult = await importExport.exportVFSSession('demo-project'); -console.log('Session export result:', exportResult); - -// Show current VFS statistics -const stats = importExport.getImportStats(); -console.log('VFS Statistics:', stats); -`, - python: ` -import os -import json - -# Explore the project structure created by JavaScript -project_root = '/workspace/myproject' - -if os.path.exists(project_root): - print("Project structure:") - - # Walk through all files and directories - for root, dirs, files in os.walk(project_root): - level = root.replace(project_root, '').count(os.sep) - indent = ' ' * 2 * level - print(f"{indent}{os.path.basename(root)}/") - - sub_indent = ' ' * 2 * (level + 1) - for file in files: - print(f"{sub_indent}{file}") - - print("\\n" + "="*50 + "\\n") - - # Read and analyze project files - package_file = os.path.join(project_root, 'package.json') - if os.path.exists(package_file): - with open(package_file, 'r') as f: - package_info = json.load(f) - print(f"Project: {package_info['name']} v{package_info['version']}") - print(f"Description: {package_info['description']}") - - # Process data files - data_dir = os.path.join(project_root, 'data') - if os.path.exists(data_dir): - print(f"\\nData files in {data_dir}:") - - for file in os.listdir(data_dir): - file_path = os.path.join(data_dir, file) - print(f" {file}") - - if file.endswith('.json'): - with open(file_path, 'r') as f: - data = json.load(f) - print(f" JSON records: {len(data)}") - elif file.endswith('.csv'): - with open(file_path, 'r') as f: - lines = f.readlines() - print(f" CSV rows: {len(lines) - 1}") # Subtract header - - # Create a Python analysis report - report = { - "analysis_date": "2024-01-01", - "project_name": package_info.get('name', 'unknown'), - "total_files": sum(len(files) for _, _, files in os.walk(project_root)), - "total_directories": sum(len(dirs) for _, dirs, _ in os.walk(project_root)), - "file_types": {} - } - - # Count file types - for root, dirs, files in os.walk(project_root): - for file in files: - ext = os.path.splitext(file)[1] or 'no_extension' - report["file_types"][ext] = report["file_types"].get(ext, 0) + 1 - - # Save analysis report - report_path = os.path.join(project_root, 'docs', 'analysis_report.json') - os.makedirs(os.path.dirname(report_path), exist_ok=True) - - with open(report_path, 'w') as f: - json.dump(report, f, indent=2) - - print(f"\\nAnalysis report saved to {report_path}") - print("Report contents:", json.dumps(report, indent=2)) - -else: - print("Project not found. Please run the JavaScript setup first.") -` - }, - - // Multiple file upload - bulkOperations: { - title: "Bulk Upload and Processing", - description: "Handle multiple files and batch operations", - javascript: ` -// Simulate multiple file upload -const files = [ - { name: 'config.json', content: '{"debug": true, "version": "1.0"}', type: 'application/json' }, - { name: 'users.csv', content: 'id,name,email\\n1,John,john@example.com\\n2,Jane,jane@example.com', type: 'text/csv' }, - { name: 'readme.txt', content: 'This is a readme file\\nwith multiple lines\\nof documentation.', type: 'text/plain' }, - { name: 'data.log', content: '2024-01-01 10:00:00 INFO Application started\\n2024-01-01 10:01:00 INFO User logged in\\n2024-01-01 10:02:00 ERROR Database connection failed', type: 'text/plain' } -]; - -console.log('Uploading multiple files...'); - -const uploadResults = []; -for (const fileInfo of files) { - // Create mock file object - const mockFile = new Blob([fileInfo.content], { type: fileInfo.type }); - mockFile.name = fileInfo.name; - - // Upload to /data directory - const result = await importExport.uploadFile(mockFile, \`/data/\${fileInfo.name}\`); - uploadResults.push(result); - - console.log(\`Upload \${fileInfo.name}:\`, result.success ? 'SUCCESS' : 'FAILED'); -} - -console.log('\\nUpload Summary:'); -const successful = uploadResults.filter(r => r.success).length; -const failed = uploadResults.filter(r => !r.success).length; -console.log(\`Successful: \${successful}, Failed: \${failed}\`); - -// Process uploaded files -console.log('\\nProcessing uploaded files...'); - -// Read and parse JSON config -const config = JSON.parse(fs.readFileSync('/data/config.json', 'utf8')); -console.log('Config loaded:', config); - -// Convert CSV to JSON -await importExport.convertToFormat('/data/users.csv', 'json', '/data/users.json'); -const users = JSON.parse(fs.readFileSync('/data/users.json', 'utf8')); -console.log('Users:', users); - -// Analyze log file -const logContent = fs.readFileSync('/data/data.log', 'utf8'); -const logLines = logContent.split('\\n').filter(line => line.trim()); -const errorCount = logLines.filter(line => line.includes('ERROR')).length; -const infoCount = logLines.filter(line => line.includes('INFO')).length; - -console.log(\`Log analysis: \${infoCount} INFO, \${errorCount} ERROR messages\`); - -// Create processing summary -const summary = { - timestamp: new Date().toISOString(), - files_processed: files.length, - config: config, - user_count: users.length, - log_stats: { info: infoCount, errors: errorCount } -}; - -fs.writeFileSync('/data/processing_summary.json', JSON.stringify(summary, null, 2)); -console.log('Processing summary saved to /data/processing_summary.json'); -`, - python: ` -import os -import json -import csv -from datetime import datetime - -# Process the uploaded files -data_dir = '/data' - -print("Processing bulk uploaded files...") - -# Check what files are available -if os.path.exists(data_dir): - files = os.listdir(data_dir) - print(f"Found {len(files)} files: {files}") - - # Read processing summary from JavaScript - summary_file = os.path.join(data_dir, 'processing_summary.json') - if os.path.exists(summary_file): - with open(summary_file, 'r') as f: - js_summary = json.load(f) - print("\\nJavaScript processing summary:", js_summary) - - # Advanced processing with Python - - # 1. Enhanced user data processing - users_file = os.path.join(data_dir, 'users.json') - if os.path.exists(users_file): - with open(users_file, 'r') as f: - users = json.load(f) - - # Add domain analysis - domains = {} - for user in users: - email = user.get('email', '') - domain = email.split('@')[-1] if '@' in email else 'unknown' - domains[domain] = domains.get(domain, 0) + 1 - - print("\\nEmail domain analysis:", domains) - - # Create enhanced user report - user_report = { - "total_users": len(users), - "domains": domains, - "users_by_domain": {domain: [u for u in users if u.get('email', '').endswith(f'@{domain}')] - for domain in domains if domain != 'unknown'} - } - - with open(os.path.join(data_dir, 'user_analysis.json'), 'w') as f: - json.dump(user_report, f, indent=2) - - # 2. Advanced log analysis - log_file = os.path.join(data_dir, 'data.log') - if os.path.exists(log_file): - with open(log_file, 'r') as f: - log_lines = f.readlines() - - # Parse log entries - log_entries = [] - for line in log_lines: - line = line.strip() - if line: - parts = line.split(' ', 3) - if len(parts) >= 4: - log_entries.append({ - 'date': parts[0], - 'time': parts[1], - 'level': parts[2], - 'message': parts[3] - }) - - # Generate log statistics - log_stats = { - 'total_entries': len(log_entries), - 'by_level': {}, - 'by_hour': {}, - 'errors': [entry for entry in log_entries if entry['level'] == 'ERROR'] - } - - for entry in log_entries: - level = entry['level'] - hour = entry['time'][:2] # Extract hour - - log_stats['by_level'][level] = log_stats['by_level'].get(level, 0) + 1 - log_stats['by_hour'][hour] = log_stats['by_hour'].get(hour, 0) + 1 - - print("\\nAdvanced log analysis:") - print(f" Total entries: {log_stats['total_entries']}") - print(f" By level: {log_stats['by_level']}") - print(f" By hour: {log_stats['by_hour']}") - - # Save detailed log analysis - with open(os.path.join(data_dir, 'log_analysis.json'), 'w') as f: - json.dump(log_stats, f, indent=2) - - # 3. Create comprehensive processing report - final_report = { - "processed_at": datetime.now().isoformat(), - "python_version": "3.x", - "total_files": len(files), - "file_list": files, - "analyses_completed": [ - "user_domain_analysis", - "log_pattern_analysis", - "configuration_validation" - ] - } - - with open(os.path.join(data_dir, 'python_processing_report.json'), 'w') as f: - json.dump(final_report, f, indent=2) - - print("\\nPython processing complete!") - print("Generated reports:") - print(" - user_analysis.json") - print(" - log_analysis.json") - print(" - python_processing_report.json") - -else: - print("Data directory not found. Please run the JavaScript upload first.") -` - } -}; - -// Export examples -if (typeof module !== 'undefined' && module.exports) { - module.exports = { vfsImportExportExamples } -} else { - window.VFSImportExportExamples = { vfsImportExportExamples } -} \ No newline at end of file diff --git a/web/src/utils/vfs-test.js b/web/src/utils/vfs-test.js deleted file mode 100644 index 89e755e2..00000000 --- a/web/src/utils/vfs-test.js +++ /dev/null @@ -1,383 +0,0 @@ -/** - * VFS Test Examples - * Demonstrates usage of the Virtual File System in both Python and JavaScript runners - */ - -// Python VFS Examples -const pythonVFSExamples = [ - { - title: "Basic File Operations", - code: ` -# Write a simple text file -with open('/data/hello.txt', 'w') as f: - f.write('Hello, Virtual File System!') - -# Read the file back -with open('/data/hello.txt', 'r') as f: - content = f.read() - print(f"File content: {content}") - -# Check if file exists -import os -print(f"File exists: {os.path.exists('/data/hello.txt')}") -` - }, - { - title: "CSV Data Processing", - code: ` -import csv -import os - -# Create sample data -data = [ - ['Name', 'Age', 'City'], - ['Alice', 25, 'New York'], - ['Bob', 30, 'San Francisco'], - ['Charlie', 35, 'Chicago'] -] - -# Write CSV file -with open('/data/people.csv', 'w', newline='') as f: - writer = csv.writer(f) - writer.writerows(data) - -# Read and process CSV -with open('/data/people.csv', 'r') as f: - reader = csv.DictReader(f) - for row in reader: - print(f"{row['Name']} is {row['Age']} years old and lives in {row['City']}") - -# List directory contents -print("Files in /data:") -for file in os.listdir('/data'): - print(f" {file}") -` - }, - { - title: "JSON Configuration", - code: ` -import json -import os - -# Create configuration -config = { - "app_name": "VFS Demo", - "version": "1.0.0", - "debug": True, - "database": { - "host": "localhost", - "port": 5432, - "name": "demo_db" - } -} - -# Create config directory -os.makedirs('/workspace/config', exist_ok=True) - -# Save configuration -with open('/workspace/config/app.json', 'w') as f: - json.dump(config, f, indent=2) - -# Load and modify configuration -with open('/workspace/config/app.json', 'r') as f: - loaded_config = json.load(f) - -loaded_config['debug'] = False -loaded_config['version'] = '1.0.1' - -# Save updated configuration -with open('/workspace/config/app.json', 'w') as f: - json.dump(loaded_config, f, indent=2) - -print("Configuration updated successfully") -print(f"New version: {loaded_config['version']}") -` - }, - { - title: "Working with pandas and VFS", - code: ` -import pandas as pd -import json - -# Create sample data -data = { - 'product': ['A', 'B', 'C', 'D', 'E'], - 'sales': [100, 150, 80, 200, 120], - 'profit': [20, 30, 15, 40, 25] -} - -df = pd.DataFrame(data) - -# Save to CSV -df.to_csv('/data/sales.csv', index=False) - -# Save to JSON -df.to_json('/data/sales.json', orient='records', indent=2) - -# Read back from CSV -df_csv = pd.read_csv('/data/sales.csv') -print("Data from CSV:") -print(df_csv) - -# Calculate summary statistics -summary = df_csv.describe() -print("\\nSummary statistics:") -print(summary) - -# Save summary -summary.to_csv('/data/sales_summary.csv') -print("\\nSummary saved to /data/sales_summary.csv") -` - }, - { - title: "File System Navigation", - code: ` -import os -from pathlib import Path - -# Show current directory -print(f"Current directory: {os.getcwd()}") - -# Create directory structure -os.makedirs('/workspace/project/src', exist_ok=True) -os.makedirs('/workspace/project/tests', exist_ok=True) -os.makedirs('/workspace/project/docs', exist_ok=True) - -# Create some files -files_to_create = [ - '/workspace/project/README.md', - '/workspace/project/src/main.py', - '/workspace/project/src/utils.py', - '/workspace/project/tests/test_main.py', - '/workspace/project/docs/api.md' -] - -for file_path in files_to_create: - with open(file_path, 'w') as f: - f.write(f"# {Path(file_path).name}\\n\\nContent for {file_path}") - -# Navigate and explore -os.chdir('/workspace/project') -print(f"\\nChanged to: {os.getcwd()}") - -print("\\nProject structure:") -for root, dirs, files in os.walk('.'): - level = root.replace('.', '').count(os.sep) - indent = ' ' * 2 * level - print(f"{indent}{os.path.basename(root)}/") - subindent = ' ' * 2 * (level + 1) - for file in files: - print(f"{subindent}{file}") -` - } -] - -// JavaScript VFS Examples -const javascriptVFSExamples = [ - { - title: "Node.js-style File Operations", - code: ` -const fs = require('fs'); - -// Write a simple text file -fs.writeFileSync('/data/greeting.txt', 'Hello from JavaScript VFS!'); - -// Read the file back -const content = fs.readFileSync('/data/greeting.txt', 'utf8'); -console.log('File content:', content); - -// Check if file exists -console.log('File exists:', fs.existsSync('/data/greeting.txt')); - -// Get file stats -const stats = fs.statSync('/data/greeting.txt'); -console.log('Is file:', stats.isFile); -console.log('Is directory:', stats.isDirectory); -` - }, - { - title: "JSON Data Management", - code: ` -const fs = require('fs'); - -// Create user data -const users = [ - { id: 1, name: 'John Doe', email: 'john@example.com', active: true }, - { id: 2, name: 'Jane Smith', email: 'jane@example.com', active: false }, - { id: 3, name: 'Bob Johnson', email: 'bob@example.com', active: true } -]; - -// Save users to JSON file -fs.writeFileSync('/data/users.json', JSON.stringify(users, null, 2)); - -// Read and filter users -const loadedUsers = JSON.parse(fs.readFileSync('/data/users.json', 'utf8')); -const activeUsers = loadedUsers.filter(user => user.active); - -console.log('Active users:', activeUsers); - -// Save filtered results -fs.writeFileSync('/data/active_users.json', JSON.stringify(activeUsers, null, 2)); - -console.log('Filtered data saved to /data/active_users.json'); -` - }, - { - title: "CSV Processing", - code: ` -const fs = require('fs'); - -// Create CSV data -const csvData = [ - 'Name,Age,Department', - 'Alice Johnson,28,Engineering', - 'Bob Smith,32,Marketing', - 'Carol Brown,29,Design', - 'David Wilson,35,Engineering' -].join('\\n'); - -// Write CSV file -fs.writeFileSync('/data/employees.csv', csvData); - -// Read and parse CSV -const csvContent = fs.readFileSync('/data/employees.csv', 'utf8'); -const lines = csvContent.split('\\n'); -const headers = lines[0].split(','); -const employees = lines.slice(1).map(line => { - const values = line.split(','); - const employee = {}; - headers.forEach((header, index) => { - employee[header] = values[index]; - }); - return employee; -}); - -console.log('Employees:', employees); - -// Filter by department -const engineers = employees.filter(emp => emp.Department === 'Engineering'); -console.log('Engineers:', engineers); - -// Calculate average age -const avgAge = employees.reduce((sum, emp) => sum + parseInt(emp.Age), 0) / employees.length; -console.log('Average age:', avgAge.toFixed(1)); -` - }, - { - title: "Directory Operations", - code: ` -const fs = require('fs'); -const path = require('path'); - -// Create directory structure -fs.mkdirSync('/workspace/myapp', { recursive: true }); -fs.mkdirSync('/workspace/myapp/src', { recursive: true }); -fs.mkdirSync('/workspace/myapp/public', { recursive: true }); -fs.mkdirSync('/workspace/myapp/config', { recursive: true }); - -// Create package.json -const packageJson = { - name: 'my-vfs-app', - version: '1.0.0', - description: 'A demo app using VFS', - main: 'src/index.js', - scripts: { - start: 'node src/index.js' - } -}; - -fs.writeFileSync('/workspace/myapp/package.json', JSON.stringify(packageJson, null, 2)); - -// Create main application file -const appCode = \` -console.log('Welcome to VFS App!'); -console.log('Current directory:', process.cwd()); - -const fs = require('fs'); -const config = JSON.parse(fs.readFileSync('./config/app.json', 'utf8')); -console.log('App config:', config); -\`; - -fs.writeFileSync('/workspace/myapp/src/index.js', appCode); - -// Create configuration -const appConfig = { - port: 3000, - environment: 'development', - features: { - logging: true, - debugging: true - } -}; - -fs.writeFileSync('/workspace/myapp/config/app.json', JSON.stringify(appConfig, null, 2)); - -// List directory contents -console.log('Project structure:'); -function listDirectory(dir, indent = '') { - const items = fs.readdirSync(dir); - items.forEach(item => { - const itemPath = path.join(dir, item); - const stats = fs.statSync(itemPath); - if (stats.isDirectory) { - console.log(\`\${indent}\${item}/\`); - listDirectory(itemPath, indent + ' '); - } else { - console.log(\`\${indent}\${item}\`); - } - }); -} - -listDirectory('/workspace/myapp'); -` - }, - { - title: "Async File Operations", - code: ` -const fs = require('fs'); - -async function demonstrateAsyncOps() { - try { - // Write multiple files asynchronously - const tasks = [ - fs.writeFile('/tmp/file1.txt', 'Content of file 1'), - fs.writeFile('/tmp/file2.txt', 'Content of file 2'), - fs.writeFile('/tmp/file3.txt', 'Content of file 3') - ]; - - await Promise.all(tasks); - console.log('All files written successfully'); - - // Read files asynchronously - const files = await fs.readdir('/tmp'); - console.log('Files in /tmp:', files); - - // Read file contents - for (const file of files) { - if (file.startsWith('file')) { - const content = await fs.readFile(\`/tmp/\${file}\`, 'utf8'); - console.log(\`\${file}: \${content}\`); - } - } - - // Demonstrate fs.promises API - const content = await fs.promises.readFile('/tmp/file1.txt', 'utf8'); - console.log('Using fs.promises:', content); - - } catch (error) { - console.error('Error:', error.message); - } -} - -// Run the async demonstration -demonstrateAsyncOps(); -` - } -] - -// Export for use in documentation -if (typeof module !== 'undefined' && module.exports) { - module.exports = { pythonVFSExamples, javascriptVFSExamples } -} else { - window.VFSExamples = { pythonVFSExamples, javascriptVFSExamples } -} \ No newline at end of file diff --git a/web/src/utils/vfsImportExport.js b/web/src/utils/vfsImportExport.js deleted file mode 100644 index c9d9a59a..00000000 --- a/web/src/utils/vfsImportExport.js +++ /dev/null @@ -1,653 +0,0 @@ -/** - * VFS Data Import/Export System - * Handles file uploads, downloads, and data format conversions - */ - -class VFSImportExport { - constructor(vfs) { - this.vfs = vfs - this.supportedFormats = { - 'text': ['.txt', '.md', '.csv', '.json', '.xml', '.yaml', '.yml', '.log'], - 'data': ['.csv', '.json', '.xlsx', '.tsv'], - 'code': ['.js', '.py', '.html', '.css', '.sql'], - 'binary': ['.png', '.jpg', '.jpeg', '.gif', '.pdf', '.zip', '.tar'] - } - } - - // ============ FILE UPLOAD FUNCTIONALITY ============ - - async uploadFile(file, targetPath = null) { - try { - // Validate file object - if (!file || !(file instanceof File) && !(file instanceof Blob)) { - throw new Error(`Invalid file object: expected File or Blob, got ${typeof file}`) - } - - // Generate target path if not provided - if (!targetPath) { - targetPath = `/data/${this.sanitizeFilename(file.name)}` - } - - // Detect file type and handle appropriately - const fileExtension = this.getFileExtension(file.name) - const isTextFile = this.isTextFile(fileExtension) - - let fileData - if (isTextFile) { - fileData = await this.readFileAsText(file) - } else { - fileData = await this.readFileAsBinary(file) - } - - // Store in VFS - await this.vfs.writeFile(targetPath, fileData, { - binary: !isTextFile, - originalName: file.name, - size: file.size, - type: file.type, - lastModified: new Date(file.lastModified) - }) - - return { - success: true, - path: targetPath, - size: file.size, - type: isTextFile ? 'text' : 'binary', - message: `File uploaded successfully to ${targetPath}` - } - } catch (error) { - return { - success: false, - error: error.message, - message: `Failed to upload ${file.name}: ${error.message}` - } - } - } - - async uploadMultipleFiles(files, targetDirectory = '/data') { - const results = [] - - // Ensure target directory exists - await this.vfs.mkdir(targetDirectory, { recursive: true }) - - for (const file of files) { - const targetPath = `${targetDirectory}/${this.sanitizeFilename(file.name)}` - const result = await this.uploadFile(file, targetPath) - results.push({ - filename: file.name, - ...result - }) - } - - return results - } - - // ============ FILE DOWNLOAD FUNCTIONALITY ============ - - async downloadFile(vfsPath, downloadName = null) { - try { - if (!await this.vfs.exists(vfsPath)) { - throw new Error(`File not found: ${vfsPath}`) - } - - const stat = await this.vfs.stat(vfsPath) - if (stat.isDirectory) { - throw new Error(`Cannot download directory: ${vfsPath}`) - } - - const fileData = await this.vfs.readFile(vfsPath, 'binary') - const filename = downloadName || this.vfs.pathResolver.basename(vfsPath) - - this.triggerDownload(fileData, filename, this.getMimeType(filename)) - - return { - success: true, - filename: filename, - size: stat.size, - message: `Downloaded ${filename} successfully` - } - } catch (error) { - return { - success: false, - error: error.message, - message: `Failed to download ${vfsPath}: ${error.message}` - } - } - } - - async downloadDirectory(vfsPath, zipName = null) { - try { - if (!await this.vfs.exists(vfsPath)) { - throw new Error(`Directory not found: ${vfsPath}`) - } - - const stat = await this.vfs.stat(vfsPath) - if (!stat.isDirectory) { - throw new Error(`Path is not a directory: ${vfsPath}`) - } - - // Collect all files in directory recursively - const files = await this.collectDirectoryFiles(vfsPath) - const zipFilename = zipName || `${this.vfs.pathResolver.basename(vfsPath)}.zip` - - // Create ZIP file - const zipBlob = await this.createZipFromFiles(files, vfsPath) - this.triggerDownload(zipBlob, zipFilename, 'application/zip') - - return { - success: true, - filename: zipFilename, - fileCount: files.length, - message: `Downloaded ${files.length} files as ${zipFilename}` - } - } catch (error) { - return { - success: false, - error: error.message, - message: `Failed to download directory ${vfsPath}: ${error.message}` - } - } - } - - // ============ DATA FORMAT CONVERSION ============ - - async convertToFormat(vfsPath, targetFormat, outputPath = null) { - try { - const sourceData = await this.vfs.readFile(vfsPath, 'utf8') - const sourceExtension = this.getFileExtension(vfsPath) - - if (!outputPath) { - const baseName = this.vfs.pathResolver.basename(vfsPath).replace(/\.[^.]+$/, '') - outputPath = `${this.vfs.pathResolver.dirname(vfsPath)}/${baseName}.${targetFormat}` - } - - let convertedData - switch (targetFormat.toLowerCase()) { - case 'json': - convertedData = await this.convertToJSON(sourceData, sourceExtension) - break - case 'csv': - convertedData = await this.convertToCSV(sourceData, sourceExtension) - break - case 'xlsx': - convertedData = await this.convertToExcel(sourceData, sourceExtension) - break - case 'xml': - convertedData = await this.convertToXML(sourceData, sourceExtension) - break - default: - throw new Error(`Unsupported target format: ${targetFormat}`) - } - - await this.vfs.writeFile(outputPath, convertedData) - - return { - success: true, - inputPath: vfsPath, - outputPath: outputPath, - format: targetFormat, - message: `Converted ${vfsPath} to ${targetFormat} format` - } - } catch (error) { - return { - success: false, - error: error.message, - message: `Failed to convert ${vfsPath}: ${error.message}` - } - } - } - - // ============ BULK OPERATIONS ============ - - async importFromURL(url, targetPath, options = {}) { - try { - const response = await fetch(url) - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`) - } - - const filename = targetPath || this.extractFilenameFromURL(url) - const contentType = response.headers.get('content-type') || '' - - let data - if (contentType.includes('text') || contentType.includes('json') || contentType.includes('xml')) { - data = await response.text() - } else { - data = await response.arrayBuffer() - } - - await this.vfs.writeFile(filename, data, { - binary: !(contentType.includes('text') || contentType.includes('json')), - source: 'url', - originalURL: url - }) - - return { - success: true, - path: filename, - url: url, - size: data.length || data.byteLength, - message: `Imported from ${url} to ${filename}` - } - } catch (error) { - return { - success: false, - error: error.message, - message: `Failed to import from ${url}: ${error.message}` - } - } - } - - async exportVFSSession(sessionName = 'vfs-session') { - try { - const sessionData = { - metadata: { - exportDate: new Date().toISOString(), - sessionName: sessionName, - version: '1.0' - }, - files: {}, - directories: Array.from(this.vfs.directories) - } - - // Export all files - for (const [path, _] of this.vfs.files) { - try { - const data = await this.vfs.readFile(path, 'binary') - const metadata = this.vfs.metadata.get(path) - - sessionData.files[path] = { - data: this.arrayBufferToBase64(data), - metadata: metadata, - encoding: 'base64' - } - } catch (error) { - console.warn(`Failed to export file ${path}:`, error) - } - } - - const sessionJSON = JSON.stringify(sessionData, null, 2) - const filename = `${sessionName}-${new Date().toISOString().slice(0, 10)}.vfs.json` - - this.triggerDownload(sessionJSON, filename, 'application/json') - - return { - success: true, - filename: filename, - fileCount: Object.keys(sessionData.files).length, - message: `Exported VFS session as ${filename}` - } - } catch (error) { - return { - success: false, - error: error.message, - message: `Failed to export VFS session: ${error.message}` - } - } - } - - async importVFSSession(file) { - try { - const sessionText = await this.readFileAsText(file) - const sessionData = JSON.parse(sessionText) - - if (!sessionData.metadata || !sessionData.files) { - throw new Error('Invalid VFS session file format') - } - - // Clear existing VFS (optional - could ask user) - this.vfs.clear() - - // Restore directories - if (sessionData.directories) { - for (const dir of sessionData.directories) { - this.vfs.directories.add(dir) - } - } - - // Restore files - let importedCount = 0 - for (const [path, fileInfo] of Object.entries(sessionData.files)) { - try { - let data - if (fileInfo.encoding === 'base64') { - data = this.base64ToArrayBuffer(fileInfo.data) - } else { - data = fileInfo.data - } - - await this.vfs.writeFile(path, data, { - binary: fileInfo.metadata?.type === 'binary', - ...fileInfo.metadata - }) - importedCount++ - } catch (error) { - console.warn(`Failed to import file ${path}:`, error) - } - } - - return { - success: true, - importedFiles: importedCount, - sessionName: sessionData.metadata.sessionName, - exportDate: sessionData.metadata.exportDate, - message: `Imported ${importedCount} files from VFS session` - } - } catch (error) { - return { - success: false, - error: error.message, - message: `Failed to import VFS session: ${error.message}` - } - } - } - - // ============ HELPER METHODS ============ - - async readFileAsText(file) { - return new Promise((resolve, reject) => { - // Validate that file is a Blob/File object - if (!(file instanceof Blob) && !(file instanceof File)) { - reject(new Error(`Expected File or Blob, but received: ${typeof file} - ${file?.constructor?.name || 'unknown'}`)) - return - } - - const reader = new FileReader() - reader.onload = e => resolve(e.target.result) - reader.onerror = e => reject(new Error('Failed to read file as text')) - reader.readAsText(file) - }) - } - - async readFileAsBinary(file) { - return new Promise((resolve, reject) => { - // Validate that file is a Blob/File object - if (!(file instanceof Blob) && !(file instanceof File)) { - reject(new Error(`Expected File or Blob, but received: ${typeof file} - ${file?.constructor?.name || 'unknown'}`)) - return - } - - const reader = new FileReader() - reader.onload = e => resolve(e.target.result) - reader.onerror = e => reject(new Error('Failed to read file as binary')) - reader.readAsArrayBuffer(file) - }) - } - - triggerDownload(data, filename, mimeType = 'application/octet-stream') { - let blob - if (data instanceof ArrayBuffer) { - blob = new Blob([data], { type: mimeType }) - } else if (typeof data === 'string') { - blob = new Blob([data], { type: mimeType }) - } else { - blob = data // Assume it's already a Blob - } - - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = filename - a.style.display = 'none' - - document.body.appendChild(a) - a.click() - document.body.removeChild(a) - - // Clean up the URL object - setTimeout(() => URL.revokeObjectURL(url), 1000) - } - - getFileExtension(filename) { - const lastDot = filename.lastIndexOf('.') - return lastDot === -1 ? '' : filename.slice(lastDot).toLowerCase() - } - - isTextFile(extension) { - return this.supportedFormats.text.includes(extension) || - this.supportedFormats.code.includes(extension) - } - - sanitizeFilename(filename) { - return filename.replace(/[<>:"|?*\x00-\x1f]/g, '_') - } - - getMimeType(filename) { - const ext = this.getFileExtension(filename) - const mimeTypes = { - '.txt': 'text/plain', - '.csv': 'text/csv', - '.json': 'application/json', - '.xml': 'application/xml', - '.html': 'text/html', - '.css': 'text/css', - '.js': 'application/javascript', - '.py': 'text/x-python', - '.md': 'text/markdown', - '.png': 'image/png', - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.gif': 'image/gif', - '.pdf': 'application/pdf', - '.zip': 'application/zip' - } - return mimeTypes[ext] || 'application/octet-stream' - } - - extractFilenameFromURL(url) { - try { - const urlObj = new URL(url) - const pathname = urlObj.pathname - const filename = pathname.split('/').pop() - return filename || 'downloaded-file' - } catch (error) { - return 'downloaded-file' - } - } - - async collectDirectoryFiles(dirPath) { - const files = [] - - const collectRecursive = async (currentPath) => { - const items = await this.vfs.readdir(currentPath) - - for (const item of items) { - const itemPath = this.vfs.pathResolver.join(currentPath, item) - const stat = await this.vfs.stat(itemPath) - - if (stat.isFile) { - files.push(itemPath) - } else if (stat.isDirectory) { - await collectRecursive(itemPath) - } - } - } - - await collectRecursive(dirPath) - return files - } - - async createZipFromFiles(filePaths, basePath) { - // Simple ZIP creation - in a real implementation, you'd use a ZIP library - const files = {} - - for (const filePath of filePaths) { - const relativePath = filePath.startsWith(basePath) - ? filePath.slice(basePath.length + 1) - : filePath - - const data = await this.vfs.readFile(filePath, 'binary') - files[relativePath] = data - } - - // For now, return a simple archive format - // In production, use a proper ZIP library like JSZip - const archiveData = JSON.stringify(files, null, 2) - return new Blob([archiveData], { type: 'application/json' }) - } - - // ============ DATA FORMAT CONVERTERS ============ - - async convertToJSON(sourceData, sourceExtension) { - switch (sourceExtension) { - case '.csv': - case '.tsv': - return this.csvToJSON(sourceData, sourceExtension === '.tsv' ? '\t' : ',') - case '.xml': - return this.xmlToJSON(sourceData) - default: - throw new Error(`Cannot convert ${sourceExtension} to JSON`) - } - } - - async convertToCSV(sourceData, sourceExtension) { - switch (sourceExtension) { - case '.json': - return this.jsonToCSV(sourceData) - case '.tsv': - return sourceData.replace(/\t/g, ',') - default: - throw new Error(`Cannot convert ${sourceExtension} to CSV`) - } - } - - csvToJSON(csvData, delimiter = ',') { - const lines = csvData.split('\n').filter(line => line.trim()) - if (lines.length === 0) return '[]' - - const headers = lines[0].split(delimiter).map(h => h.trim()) - const records = [] - - for (let i = 1; i < lines.length; i++) { - const values = lines[i].split(delimiter) - const record = {} - - headers.forEach((header, index) => { - record[header] = values[index]?.trim() || '' - }) - - records.push(record) - } - - return JSON.stringify(records, null, 2) - } - - jsonToCSV(jsonData) { - const data = JSON.parse(jsonData) - if (!Array.isArray(data) || data.length === 0) { - throw new Error('JSON must be an array of objects') - } - - const headers = Object.keys(data[0]) - const csvLines = [headers.join(',')] - - for (const record of data) { - const values = headers.map(header => { - const value = record[header] || '' - // Escape commas and quotes - return value.toString().includes(',') ? `"${value}"` : value - }) - csvLines.push(values.join(',')) - } - - return csvLines.join('\n') - } - - xmlToJSON(xmlData) { - // Basic XML to JSON conversion - // In production, use a proper XML parser - try { - const parser = new DOMParser() - const xmlDoc = parser.parseFromString(xmlData, 'text/xml') - const result = this.xmlNodeToObject(xmlDoc.documentElement) - return JSON.stringify(result, null, 2) - } catch (error) { - throw new Error(`Failed to parse XML: ${error.message}`) - } - } - - xmlNodeToObject(node) { - const result = {} - - // Handle attributes - if (node.attributes && node.attributes.length > 0) { - result['@attributes'] = {} - for (const attr of node.attributes) { - result['@attributes'][attr.name] = attr.value - } - } - - // Handle child nodes - const children = Array.from(node.childNodes) - const textContent = children - .filter(child => child.nodeType === Node.TEXT_NODE) - .map(child => child.textContent.trim()) - .filter(text => text) - .join(' ') - - if (textContent) { - result['#text'] = textContent - } - - const elementChildren = children.filter(child => child.nodeType === Node.ELEMENT_NODE) - for (const child of elementChildren) { - const childName = child.nodeName - const childValue = this.xmlNodeToObject(child) - - if (result[childName]) { - if (!Array.isArray(result[childName])) { - result[childName] = [result[childName]] - } - result[childName].push(childValue) - } else { - result[childName] = childValue - } - } - - return result - } - - // ============ UTILITY METHODS ============ - - arrayBufferToBase64(buffer) { - const bytes = new Uint8Array(buffer) - let binary = '' - for (let i = 0; i < bytes.byteLength; i++) { - binary += String.fromCharCode(bytes[i]) - } - return btoa(binary) - } - - base64ToArrayBuffer(base64) { - const binary = atob(base64) - const bytes = new Uint8Array(binary.length) - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.charCodeAt(i) - } - return bytes.buffer - } - - getImportStats() { - const stats = { - totalFiles: this.vfs.files.size, - totalDirectories: this.vfs.directories.size, - totalSize: 0, - fileTypes: {} - } - - for (const [path, _] of this.vfs.files) { - const metadata = this.vfs.metadata.get(path) - const size = metadata?.size || 0 - stats.totalSize += size - - const ext = this.getFileExtension(path) - stats.fileTypes[ext] = (stats.fileTypes[ext] || 0) + 1 - } - - return stats - } -} - -// Export for use in other modules -export default VFSImportExport -export { VFSImportExport } \ No newline at end of file diff --git a/web/src/utils/virtualFileSystem.js b/web/src/utils/virtualFileSystem.js deleted file mode 100644 index 7828087d..00000000 --- a/web/src/utils/virtualFileSystem.js +++ /dev/null @@ -1,858 +0,0 @@ -/** - * Virtual File System (VFS) Implementation - * Provides secure, isolated file system operations for code runners - */ - -class VirtualFileSystem { - constructor(options = {}) { - // Core storage - this.files = new Map() // file path -> file data - this.directories = new Set(['/']).add('/') // directory paths - this.metadata = new Map() // file metadata (size, modified, etc.) - this.currentDirectory = '/' // current working directory - - // Resource limits - this.maxFileSize = options.maxFileSize || 10 * 1024 * 1024 // 10MB per file - this.maxTotalSize = options.maxTotalSize || 100 * 1024 * 1024 // 100MB total - this.maxFiles = options.maxFiles || 1000 // Maximum number of files - - // Components - this.pathResolver = new PathResolver() - this.security = new VFSSecurity(this) - this.utils = new FileSystemUtils(this) - this.persistence = new VFSPersistence(this) - - // Import/Export will be initialized externally - this.importExport = null - - // Initialize root directory - this.directories.add('/') - this._updateDirectoryMetadata('/') - } - - // ============ CORE FILE OPERATIONS ============ - - async writeFile(filePath, data, options = {}) { - try { - const normalizedPath = this.pathResolver.normalize(filePath) - this.security.validatePath(normalizedPath) - - // Convert data to appropriate format - let fileData, size - if (options.binary || data instanceof ArrayBuffer || data instanceof Uint8Array) { - // Handle binary data - if (data instanceof ArrayBuffer) { - fileData = new Uint8Array(data) - } else if (data instanceof Uint8Array) { - fileData = data - } else { - fileData = new TextEncoder().encode(data) - } - size = fileData.byteLength - } else { - // Handle text data - fileData = String(data) - size = new TextEncoder().encode(fileData).byteLength - } - - // Check quotas - const currentSize = this._getTotalSize() - this.security.checkQuota(currentSize, size) - - // Ensure parent directory exists - const parentDir = this.pathResolver.dirname(normalizedPath) - if (!this.directories.has(parentDir)) { - await this.mkdir(parentDir, { recursive: true }) - } - - // Store file - this.files.set(normalizedPath, fileData) - this._updateFileMetadata(normalizedPath, { - size: size, - type: options.binary ? 'binary' : 'text', - encoding: options.encoding || 'utf8', - mtime: new Date(), - created: this.metadata.get(normalizedPath)?.created || new Date() - }) - - return normalizedPath - } catch (error) { - throw new Error(`Failed to write file ${filePath}: ${error.message}`) - } - } - - async readFile(filePath, encoding = 'utf8') { - try { - const normalizedPath = this.pathResolver.normalize(filePath) - this.security.validatePath(normalizedPath) - - if (!this.files.has(normalizedPath)) { - throw new Error(`File not found: ${filePath}`) - } - - const fileData = this.files.get(normalizedPath) - const metadata = this.metadata.get(normalizedPath) - - // Return data in requested format - if (encoding === 'binary' || encoding === null) { - return fileData - } else if (metadata?.type === 'binary' && fileData instanceof Uint8Array) { - return new TextDecoder(encoding).decode(fileData) - } else { - return String(fileData) - } - } catch (error) { - throw new Error(`Failed to read file ${filePath}: ${error.message}`) - } - } - - async mkdir(dirPath, options = {}) { - try { - const normalizedPath = this.pathResolver.normalize(dirPath) - this.security.validatePath(normalizedPath) - - if (this.directories.has(normalizedPath)) { - if (!options.recursive) { - throw new Error(`Directory already exists: ${dirPath}`) - } - return normalizedPath - } - - // Create parent directories if recursive - if (options.recursive) { - const parts = normalizedPath.split('/').filter(Boolean) - let currentPath = '/' - - for (const part of parts) { - currentPath = this.pathResolver.join(currentPath, part) - if (!this.directories.has(currentPath)) { - this.directories.add(currentPath) - this._updateDirectoryMetadata(currentPath) - } - } - } else { - // Check parent exists - const parentDir = this.pathResolver.dirname(normalizedPath) - if (!this.directories.has(parentDir)) { - throw new Error(`Parent directory does not exist: ${parentDir}`) - } - - this.directories.add(normalizedPath) - this._updateDirectoryMetadata(normalizedPath) - } - - return normalizedPath - } catch (error) { - throw new Error(`Failed to create directory ${dirPath}: ${error.message}`) - } - } - - async readdir(dirPath = this.currentDirectory) { - try { - const normalizedPath = this.pathResolver.normalize(dirPath) - this.security.validatePath(normalizedPath) - - if (!this.directories.has(normalizedPath)) { - throw new Error(`Directory not found: ${dirPath}`) - } - - const items = [] - - // Find immediate children (files and directories) - const pathPrefix = normalizedPath === '/' ? '/' : normalizedPath + '/' - - // Add child directories - for (const dir of this.directories) { - if (dir !== normalizedPath && dir.startsWith(pathPrefix)) { - const relativePath = dir.slice(pathPrefix.length) - if (!relativePath.includes('/')) { // Immediate child only - items.push(relativePath) - } - } - } - - // Add files - for (const file of this.files.keys()) { - if (file.startsWith(pathPrefix)) { - const relativePath = file.slice(pathPrefix.length) - if (!relativePath.includes('/')) { // Immediate child only - items.push(relativePath) - } - } - } - - return items.sort() - } catch (error) { - throw new Error(`Failed to read directory ${dirPath}: ${error.message}`) - } - } - - async stat(itemPath) { - try { - const normalizedPath = this.pathResolver.normalize(itemPath) - this.security.validatePath(normalizedPath) - - // Check if it's a directory - if (this.directories.has(normalizedPath)) { - const metadata = this.metadata.get(normalizedPath) || {} - return { - isDirectory: true, - isFile: false, - size: 0, - mtime: metadata.mtime || new Date(), - created: metadata.created || new Date(), - path: normalizedPath - } - } - - // Check if it's a file - if (this.files.has(normalizedPath)) { - const metadata = this.metadata.get(normalizedPath) || {} - return { - isDirectory: false, - isFile: true, - size: metadata.size || 0, - type: metadata.type || 'text', - encoding: metadata.encoding || 'utf8', - mtime: metadata.mtime || new Date(), - created: metadata.created || new Date(), - path: normalizedPath - } - } - - throw new Error(`Path not found: ${itemPath}`) - } catch (error) { - throw new Error(`Failed to stat ${itemPath}: ${error.message}`) - } - } - - async exists(itemPath) { - try { - const normalizedPath = this.pathResolver.normalize(itemPath) - this.security.validatePath(normalizedPath) - return this.directories.has(normalizedPath) || this.files.has(normalizedPath) - } catch (error) { - return false - } - } - - async unlink(filePath) { - try { - const normalizedPath = this.pathResolver.normalize(filePath) - this.security.validatePath(normalizedPath) - - if (!this.files.has(normalizedPath)) { - throw new Error(`File not found: ${filePath}`) - } - - this.files.delete(normalizedPath) - this.metadata.delete(normalizedPath) - - return normalizedPath - } catch (error) { - throw new Error(`Failed to delete file ${filePath}: ${error.message}`) - } - } - - async rmdir(dirPath, options = {}) { - try { - const normalizedPath = this.pathResolver.normalize(dirPath) - this.security.validatePath(normalizedPath) - - if (normalizedPath === '/') { - throw new Error('Cannot delete root directory') - } - - if (!this.directories.has(normalizedPath)) { - throw new Error(`Directory not found: ${dirPath}`) - } - - // Check if directory is empty (unless recursive) - if (!options.recursive) { - const items = await this.readdir(normalizedPath) - if (items.length > 0) { - throw new Error(`Directory not empty: ${dirPath}`) - } - } else { - // Recursively delete contents - const items = await this.readdir(normalizedPath) - for (const item of items) { - const itemPath = this.pathResolver.join(normalizedPath, item) - const itemStat = await this.stat(itemPath) - - if (itemStat.isDirectory) { - await this.rmdir(itemPath, { recursive: true }) - } else { - await this.unlink(itemPath) - } - } - } - - this.directories.delete(normalizedPath) - this.metadata.delete(normalizedPath) - - return normalizedPath - } catch (error) { - throw new Error(`Failed to remove directory ${dirPath}: ${error.message}`) - } - } - - // ============ NAVIGATION OPERATIONS ============ - - chdir(dirPath) { - const normalizedPath = this.pathResolver.normalize(dirPath) - this.security.validatePath(normalizedPath) - - if (!this.directories.has(normalizedPath)) { - throw new Error(`Directory not found: ${dirPath}`) - } - - this.currentDirectory = normalizedPath - return normalizedPath - } - - getcwd() { - return this.currentDirectory - } - - // ============ ADVANCED OPERATIONS ============ - - async copy(srcPath, destPath) { - try { - const srcNormalized = this.pathResolver.normalize(srcPath) - const destNormalized = this.pathResolver.normalize(destPath) - - this.security.validatePath(srcNormalized) - this.security.validatePath(destNormalized) - - const srcStat = await this.stat(srcNormalized) - - if (srcStat.isFile) { - const data = await this.readFile(srcNormalized, 'binary') - const metadata = this.metadata.get(srcNormalized) - await this.writeFile(destNormalized, data, { - binary: metadata?.type === 'binary', - encoding: metadata?.encoding - }) - } else if (srcStat.isDirectory) { - await this.mkdir(destNormalized, { recursive: true }) - const items = await this.readdir(srcNormalized) - - for (const item of items) { - const srcItem = this.pathResolver.join(srcNormalized, item) - const destItem = this.pathResolver.join(destNormalized, item) - await this.copy(srcItem, destItem) - } - } - - return destNormalized - } catch (error) { - throw new Error(`Failed to copy ${srcPath} to ${destPath}: ${error.message}`) - } - } - - async move(srcPath, destPath) { - try { - await this.copy(srcPath, destPath) - - const srcStat = await this.stat(srcPath) - if (srcStat.isDirectory) { - await this.rmdir(srcPath, { recursive: true }) - } else { - await this.unlink(srcPath) - } - - return destPath - } catch (error) { - throw new Error(`Failed to move ${srcPath} to ${destPath}: ${error.message}`) - } - } - - async glob(pattern, options = {}) { - const matches = [] - const regex = this._globToRegex(pattern) - - // Search files - for (const filePath of this.files.keys()) { - if (regex.test(filePath)) { - matches.push(filePath) - } - } - - // Search directories if requested - if (options.includeDirectories !== false) { - for (const dirPath of this.directories) { - if (dirPath !== '/' && regex.test(dirPath)) { - matches.push(dirPath) - } - } - } - - return matches.sort() - } - - // ============ UTILITY METHODS ============ - - getStorageInfo() { - const totalSize = this._getTotalSize() - const fileCount = this.files.size - const dirCount = this.directories.size - - return { - totalSize, - fileCount, - dirCount, - maxFileSize: this.maxFileSize, - maxTotalSize: this.maxTotalSize, - maxFiles: this.maxFiles, - usage: { - size: (totalSize / this.maxTotalSize * 100).toFixed(1) + '%', - files: (fileCount / this.maxFiles * 100).toFixed(1) + '%' - } - } - } - - clear() { - this.files.clear() - this.directories.clear() - this.metadata.clear() - this.currentDirectory = '/' - this.directories.add('/') - this._updateDirectoryMetadata('/') - } - - // ============ PRIVATE METHODS ============ - - _getTotalSize() { - let total = 0 - for (const metadata of this.metadata.values()) { - total += metadata.size || 0 - } - return total - } - - _updateFileMetadata(path, metadata) { - const existing = this.metadata.get(path) || {} - this.metadata.set(path, { ...existing, ...metadata }) - } - - _updateDirectoryMetadata(path) { - const existing = this.metadata.get(path) || {} - this.metadata.set(path, { - ...existing, - size: 0, - mtime: new Date(), - created: existing.created || new Date() - }) - } - - _globToRegex(pattern) { - // Convert glob pattern to regex - let regex = pattern - .replace(/\./g, '\\.') - .replace(/\*/g, '[^/]*') - .replace(/\*\*/g, '.*') - .replace(/\?/g, '[^/]') - - return new RegExp(`^${regex}$`) - } - - // ============ EXPORT/IMPORT METHODS ============ - - /** - * Export VFS state for synchronization with workers - */ - export() { - return { - files: new Map(this.files), - directories: new Set(this.directories), - metadata: new Map(this.metadata), - currentDirectory: this.currentDirectory - } - } - - /** - * Import VFS state (for synchronization from main thread) - */ - import(state) { - if (state && state.files && state.directories) { - this.files.clear() - this.directories.clear() - this.metadata.clear() - - // Import files - if (state.files instanceof Map) { - for (const [path, data] of state.files) { - this.files.set(path, data) - } - } - - // Import directories - if (state.directories instanceof Set) { - for (const dir of state.directories) { - this.directories.add(dir) - } - } - - // Import metadata - if (state.metadata instanceof Map) { - for (const [path, meta] of state.metadata) { - this.metadata.set(path, meta) - } - } - - // Import current directory - if (state.currentDirectory) { - this.currentDirectory = state.currentDirectory - } - } - } -} - -// ============ PATH RESOLVER ============ - -class PathResolver { - normalize(path) { - if (!path) return '/' - - // Convert to string and handle basic cases - path = String(path).replace(/\\/g, '/') - - if (!path.startsWith('/')) { - path = '/' + path - } - - // Split into parts and resolve . and .. - const parts = path.split('/').filter(Boolean) - const resolved = [] - - for (const part of parts) { - if (part === '.') { - continue - } else if (part === '..') { - resolved.pop() - } else { - resolved.push(part) - } - } - - return '/' + resolved.join('/') - } - - resolve(path) { - return this.normalize(path) - } - - dirname(path) { - const normalized = this.normalize(path) - if (normalized === '/') return '/' - - const lastSlash = normalized.lastIndexOf('/') - if (lastSlash === 0) return '/' - - return normalized.slice(0, lastSlash) - } - - basename(path) { - const normalized = this.normalize(path) - if (normalized === '/') return '/' - - const lastSlash = normalized.lastIndexOf('/') - return normalized.slice(lastSlash + 1) - } - - extname(path) { - const base = this.basename(path) - const lastDot = base.lastIndexOf('.') - - if (lastDot === -1 || lastDot === 0) return '' - return base.slice(lastDot) - } - - join(...parts) { - const joined = parts.join('/') - return this.normalize(joined) - } - - split(path) { - const normalized = this.normalize(path) - if (normalized === '/') return ['/'] - - return normalized.split('/').filter(Boolean) - } - - validate(path) { - // Basic validation - detailed validation in VFSSecurity - if (typeof path !== 'string') { - throw new Error('Path must be a string') - } - - if (path.length > 260) { - throw new Error('Path too long') - } - - return true - } -} - -// ============ SECURITY CLASS ============ - -class VFSSecurity { - constructor(vfs) { - this.vfs = vfs - this.maxPathLength = 260 - this.maxFilenameLength = 255 - this.forbiddenChars = /[<>:"|?*\x00-\x1f]/ - this.reservedNames = new Set([ - 'CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', - 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', - 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9' - ]) - } - - validatePath(path) { - // Prevent directory traversal - if (path.includes('..')) { - throw new Error('Path traversal detected') - } - - if (path.length > this.maxPathLength) { - throw new Error(`Path too long: ${path.length} > ${this.maxPathLength}`) - } - - // Check for dangerous patterns - if (path.match(/\/\.{2,}\//)) { - throw new Error('Invalid path pattern detected') - } - - return true - } - - checkQuota(currentSize, additionalSize) { - if (additionalSize > this.vfs.maxFileSize) { - throw new Error(`File too large: ${this.formatSize(additionalSize)} > ${this.formatSize(this.vfs.maxFileSize)}`) - } - - const totalSize = currentSize + additionalSize - if (totalSize > this.vfs.maxTotalSize) { - throw new Error(`Storage quota exceeded: ${this.formatSize(totalSize)} > ${this.formatSize(this.vfs.maxTotalSize)}`) - } - - if (this.vfs.files.size >= this.vfs.maxFiles) { - throw new Error(`File count limit exceeded: ${this.vfs.files.size} >= ${this.vfs.maxFiles}`) - } - - return true - } - - sanitizeFilename(name) { - let sanitized = name.replace(this.forbiddenChars, '_') - - const baseName = sanitized.split('.')[0].toUpperCase() - if (this.reservedNames.has(baseName)) { - sanitized = `_${sanitized}` - } - - if (sanitized.length > this.maxFilenameLength) { - const ext = this.vfs.pathResolver.extname(sanitized) - const maxBase = this.maxFilenameLength - ext.length - const base = sanitized.slice(0, maxBase) - sanitized = base + ext - } - - return sanitized - } - - formatSize(bytes) { - const units = ['B', 'KB', 'MB', 'GB'] - let size = bytes - let unitIndex = 0 - - while (size >= 1024 && unitIndex < units.length - 1) { - size /= 1024 - unitIndex++ - } - - return `${size.toFixed(1)}${units[unitIndex]}` - } -} - -// ============ FILE SYSTEM UTILITIES ============ - -class FileSystemUtils { - constructor(vfs) { - this.vfs = vfs - } - - async find(pattern, options = {}) { - return await this.vfs.glob(pattern, options) - } - - async grep(pattern, files, options = {}) { - const results = [] - const regex = new RegExp(pattern, options.flags || 'gi') - - for (const file of files) { - try { - const content = await this.vfs.readFile(file, 'utf8') - const matches = [...content.matchAll(regex)] - - if (matches.length > 0) { - results.push({ - file, - matches: matches.map(match => ({ - text: match[0], - index: match.index, - groups: match.slice(1) - })) - }) - } - } catch (error) { - // Skip files that can't be read as text - continue - } - } - - return results - } - - async bulkCopy(srcPattern, destDir, options = {}) { - const files = await this.find(srcPattern) - const results = [] - - await this.vfs.mkdir(destDir, { recursive: true }) - - for (const file of files) { - const basename = this.vfs.pathResolver.basename(file) - const destPath = this.vfs.pathResolver.join(destDir, basename) - - try { - await this.vfs.copy(file, destPath) - results.push({ src: file, dest: destPath, success: true }) - } catch (error) { - results.push({ src: file, dest: destPath, success: false, error: error.message }) - } - } - - return results - } - - async bulkDelete(pattern, options = {}) { - const files = await this.find(pattern) - - if (!options.force && files.length > 10) { - throw new Error(`Bulk delete would affect ${files.length} files. Use {force: true} to proceed.`) - } - - const results = [] - - for (const file of files) { - try { - const stat = await this.vfs.stat(file) - if (stat.isDirectory) { - await this.vfs.rmdir(file, { recursive: true }) - } else { - await this.vfs.unlink(file) - } - results.push({ path: file, success: true }) - } catch (error) { - results.push({ path: file, success: false, error: error.message }) - } - } - - return results - } -} - -// ============ PERSISTENCE CLASS ============ - -class VFSPersistence { - constructor(vfs) { - this.vfs = vfs - this.storageKey = 'vfs_session' - } - - async saveSession(name = 'default') { - const sessionData = { - version: '1.0', - timestamp: new Date().toISOString(), - files: Object.fromEntries(this.vfs.files), - directories: Array.from(this.vfs.directories), - metadata: Object.fromEntries(this.vfs.metadata), - currentDirectory: this.vfs.currentDirectory - } - - try { - const jsonData = JSON.stringify(sessionData) - localStorage.setItem(`${this.storageKey}_${name}`, jsonData) - - return { - name, - size: jsonData.length, - timestamp: sessionData.timestamp - } - } catch (error) { - throw new Error(`Failed to save session: ${error.message}`) - } - } - - async loadSession(name = 'default') { - try { - const jsonData = localStorage.getItem(`${this.storageKey}_${name}`) - if (!jsonData) { - throw new Error(`Session '${name}' not found`) - } - - const sessionData = JSON.parse(jsonData) - - // Restore VFS state - this.vfs.files = new Map(Object.entries(sessionData.files)) - this.vfs.directories = new Set(sessionData.directories) - this.vfs.metadata = new Map(Object.entries(sessionData.metadata)) - this.vfs.currentDirectory = sessionData.currentDirectory - - return sessionData - } catch (error) { - throw new Error(`Failed to load session: ${error.message}`) - } - } - - listSessions() { - const sessions = [] - - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i) - if (key.startsWith(this.storageKey + '_')) { - const name = key.slice((this.storageKey + '_').length) - const data = localStorage.getItem(key) - - try { - const sessionData = JSON.parse(data) - sessions.push({ - name, - timestamp: sessionData.timestamp, - size: data.length - }) - } catch (error) { - // Skip corrupted sessions - continue - } - } - } - - return sessions.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)) - } - - deleteSession(name) { - const key = `${this.storageKey}_${name}` - if (localStorage.getItem(key)) { - localStorage.removeItem(key) - return true - } - return false - } -} - -// Export the VFS class -export default VirtualFileSystem -export { VirtualFileSystem } \ No newline at end of file diff --git a/web/src/views/chat/components/ArtifactGallery.vue b/web/src/views/chat/components/ArtifactGallery.vue index 878407c5..426ee688 100644 --- a/web/src/views/chat/components/ArtifactGallery.vue +++ b/web/src/views/chat/components/ArtifactGallery.vue @@ -8,346 +8,92 @@ -
-
- - - - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - - - - Grid - - - - List - - -
+ + + + + +
- -
-
-
-
{{ galleryStats.totalArtifacts }}
-
Total Artifacts
-
-
-
{{ galleryStats.totalExecutions }}
-
Total Executions
-
-
-
{{ Math.round(galleryStats.averageExecutionTime) }}ms
-
Avg Execution Time
-
-
-
{{ Math.round(galleryStats.successRate * 100) }}%
-
Success Rate
-
-
-
-
-

Artifacts by Type

-
-
-
{{ type }}
-
-
-
-
{{ count }}
-
-
-
-
-

Language Distribution

-
-
- - {{ count }} -
-
-
-
+
+ +

No artifacts found

+

Artifacts created in chat messages will appear here.

- -