97 lines
3 KiB
Python
97 lines
3 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""Data models for the CodeEditor feature."""
|
|
|
|
from typing import List, Dict, Any, Optional
|
|
from enum import Enum
|
|
from pydantic import BaseModel, Field
|
|
from modules.shared.timeUtils import getUtcTimestamp
|
|
import uuid
|
|
|
|
|
|
class SegmentTypeEnum(str, Enum):
|
|
TEXT = "text"
|
|
CODE_BLOCK = "code_block"
|
|
FILE_EDIT = "file_edit"
|
|
|
|
|
|
class EditStatusEnum(str, Enum):
|
|
PENDING = "pending"
|
|
ACCEPTED = "accepted"
|
|
REJECTED = "rejected"
|
|
|
|
|
|
class FileContext(BaseModel):
|
|
"""A text file loaded as context for the AI."""
|
|
fileId: str
|
|
fileName: str
|
|
content: Optional[str] = None
|
|
mimeType: str
|
|
sizeBytes: int = 0
|
|
tags: List[str] = Field(default_factory=list)
|
|
|
|
|
|
class ResponseSegment(BaseModel):
|
|
"""A parsed segment from the AI response."""
|
|
type: SegmentTypeEnum
|
|
content: str
|
|
language: Optional[str] = None
|
|
fileId: Optional[str] = None
|
|
fileName: Optional[str] = None
|
|
oldContent: Optional[str] = None
|
|
newContent: Optional[str] = None
|
|
|
|
|
|
class FileEditProposal(BaseModel):
|
|
"""A proposed file edit from the AI, awaiting user accept/reject."""
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
workflowId: str
|
|
fileId: str
|
|
fileName: str
|
|
operation: str = "edit"
|
|
oldContent: Optional[str] = None
|
|
newContent: str
|
|
diffSummary: Optional[str] = None
|
|
status: EditStatusEnum = EditStatusEnum.PENDING
|
|
createdAt: float = Field(default_factory=getUtcTimestamp)
|
|
|
|
|
|
class FileVersion(BaseModel):
|
|
"""A new version of a file created after accepting an edit proposal."""
|
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
sourceFileId: str
|
|
editProposalId: str
|
|
newFileId: str
|
|
createdAt: float = Field(default_factory=getUtcTimestamp)
|
|
|
|
|
|
TEXT_MIME_TYPES = {
|
|
"text/plain", "text/markdown", "text/html", "text/css", "text/csv",
|
|
"text/xml", "text/yaml", "text/x-python", "text/x-java",
|
|
"text/javascript", "text/x-typescript", "text/x-sql",
|
|
"application/json", "application/xml", "application/yaml",
|
|
"application/x-yaml", "application/javascript",
|
|
}
|
|
|
|
TEXT_EXTENSIONS = {
|
|
".md", ".txt", ".json", ".yaml", ".yml", ".xml", ".csv",
|
|
".py", ".js", ".ts", ".tsx", ".jsx", ".html", ".htm", ".css", ".scss",
|
|
".sql", ".sh", ".bash", ".zsh", ".ps1", ".bat",
|
|
".toml", ".ini", ".cfg", ".conf", ".env", ".gitignore",
|
|
".dockerfile", ".docker-compose", ".makefile",
|
|
".java", ".kt", ".go", ".rs", ".rb", ".php", ".swift", ".c", ".cpp", ".h",
|
|
".r", ".lua", ".dart", ".vue", ".svelte",
|
|
}
|
|
|
|
|
|
def isTextFile(mimeType: Optional[str], fileName: Optional[str] = None) -> bool:
|
|
"""Check if a file is a text-based file suitable for the editor."""
|
|
if mimeType and mimeType.lower() in TEXT_MIME_TYPES:
|
|
return True
|
|
if mimeType and mimeType.lower().startswith("text/"):
|
|
return True
|
|
if fileName:
|
|
ext = "." + fileName.rsplit(".", 1)[-1].lower() if "." in fileName else ""
|
|
if ext in TEXT_EXTENSIONS:
|
|
return True
|
|
return False
|