feat: multi-file context, context tabs
- added context tabs - added multifile context including file and image uploads to the context along with all the files from the project - added file/image previews on input - added code paste from the editor and file lines recognition - added image paste from clipboard and preview
This commit is contained in:
parent
2317cf49e9
commit
fac1404e14
@ -1,27 +1,10 @@
|
||||
import { Send, StopCircle, AtSign, Image as ImageIcon } from "lucide-react"
|
||||
import { Send, StopCircle, Image as ImageIcon, Paperclip } from "lucide-react"
|
||||
import { Button } from "../../ui/button"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../ui/select"
|
||||
import { useRef, useEffect, useState } from "react"
|
||||
import * as monaco from 'monaco-editor'
|
||||
import { useEffect } from "react"
|
||||
import { TFile, TFolder } from "@/lib/types"
|
||||
|
||||
interface ChatInputProps {
|
||||
input: string
|
||||
setInput: (input: string) => void
|
||||
isGenerating: boolean
|
||||
handleSend: (useFullContext?: boolean) => void
|
||||
handleStopGeneration: () => void
|
||||
onImageUpload: (file: File) => void
|
||||
onFileMention: (fileName: string) => void
|
||||
addContextTab: (type: string, title: string, content: string, lineRange?: { start: number, end: number }) => void
|
||||
activeFileName?: string
|
||||
editorRef: React.MutableRefObject<monaco.editor.IStandaloneCodeEditor | undefined>
|
||||
lastCopiedRangeRef: React.MutableRefObject<{ startLine: number; endLine: number } | null>
|
||||
contextTabs: { id: string; type: string; title: string; content: string; lineRange?: { start: number; end: number } }[]
|
||||
onRemoveTab: (id: string) => void
|
||||
textareaRef: React.RefObject<HTMLTextAreaElement>
|
||||
files: (TFile | TFolder)[]
|
||||
}
|
||||
import { ALLOWED_FILE_TYPES } from "./types"
|
||||
import { looksLikeCode } from "./lib/chatUtils"
|
||||
import { ChatInputProps } from "./types"
|
||||
|
||||
export default function ChatInput({
|
||||
input,
|
||||
@ -30,7 +13,6 @@ export default function ChatInput({
|
||||
handleSend,
|
||||
handleStopGeneration,
|
||||
onImageUpload,
|
||||
onFileMention,
|
||||
addContextTab,
|
||||
activeFileName,
|
||||
editorRef,
|
||||
@ -38,8 +20,8 @@ export default function ChatInput({
|
||||
contextTabs,
|
||||
onRemoveTab,
|
||||
textareaRef,
|
||||
files,
|
||||
}: ChatInputProps) {
|
||||
|
||||
// Auto-resize textarea as content changes
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
@ -48,6 +30,7 @@ export default function ChatInput({
|
||||
}
|
||||
}, [input])
|
||||
|
||||
// Handle keyboard events for sending messages
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
if (e.ctrlKey) {
|
||||
@ -65,6 +48,7 @@ export default function ChatInput({
|
||||
}
|
||||
}
|
||||
|
||||
// Handle paste events for image and code
|
||||
const handlePaste = async (e: React.ClipboardEvent) => {
|
||||
// Handle image paste
|
||||
const items = Array.from(e.clipboardData.items);
|
||||
@ -76,12 +60,17 @@ export default function ChatInput({
|
||||
if (!file) continue;
|
||||
|
||||
try {
|
||||
// Convert image to base64 string for context tab title and timestamp
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const base64String = reader.result as string;
|
||||
addContextTab(
|
||||
"image",
|
||||
`Image ${new Date().toLocaleTimeString()}`,
|
||||
`Image ${new Date().toLocaleTimeString('en-US', {
|
||||
hour12: true,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}).replace(/(\d{2}):(\d{2})/, '$1:$2')}`,
|
||||
base64String
|
||||
);
|
||||
};
|
||||
@ -93,26 +82,8 @@ export default function ChatInput({
|
||||
}
|
||||
}
|
||||
|
||||
// Get text from clipboard
|
||||
const text = e.clipboardData.getData('text');
|
||||
|
||||
// Helper function to detect if text looks like code
|
||||
const looksLikeCode = (text: string): boolean => {
|
||||
const codeIndicators = [
|
||||
/^import\s+/m, // import statements
|
||||
/^function\s+/m, // function declarations
|
||||
/^class\s+/m, // class declarations
|
||||
/^const\s+/m, // const declarations
|
||||
/^let\s+/m, // let declarations
|
||||
/^var\s+/m, // var declarations
|
||||
/[{}\[\]();]/, // common code syntax
|
||||
/^\s*\/\//m, // comments
|
||||
/^\s*\/\*/m, // multi-line comments
|
||||
/=>/, // arrow functions
|
||||
/^export\s+/m, // export statements
|
||||
];
|
||||
|
||||
return codeIndicators.some(pattern => pattern.test(text));
|
||||
};
|
||||
|
||||
// If text doesn't contain newlines or doesn't look like code, let it paste normally
|
||||
if (!text || !text.includes('\n') || !looksLikeCode(text)) {
|
||||
@ -123,6 +94,8 @@ export default function ChatInput({
|
||||
const editor = editorRef.current;
|
||||
const currentSelection = editor?.getSelection();
|
||||
const lines = text.split('\n');
|
||||
|
||||
// TODO: FIX THIS: even when i paste the outside code, it shows the active file name,it works when no tabs are open, just does not work when the tab is open
|
||||
|
||||
// If selection exists in editor, use file name and line numbers
|
||||
if (currentSelection && !currentSelection.isEmpty()) {
|
||||
@ -156,6 +129,7 @@ export default function ChatInput({
|
||||
);
|
||||
};
|
||||
|
||||
// Handle image upload from local machine via input
|
||||
const handleImageUpload = () => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
@ -167,32 +141,7 @@ export default function ChatInput({
|
||||
input.click()
|
||||
}
|
||||
|
||||
const handleMentionClick = () => {
|
||||
if (textareaRef.current) {
|
||||
const cursorPosition = textareaRef.current.selectionStart
|
||||
const newValue = input.slice(0, cursorPosition) + '@' + input.slice(cursorPosition)
|
||||
setInput(newValue)
|
||||
// Focus and move cursor after the @
|
||||
textareaRef.current.focus()
|
||||
const newPosition = cursorPosition + 1
|
||||
textareaRef.current.setSelectionRange(newPosition, newPosition)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle @ mentions in input
|
||||
useEffect(() => {
|
||||
const match = input.match(/@(\w+)$/)
|
||||
if (match) {
|
||||
const fileName = match[1]
|
||||
const allFiles = getAllFiles(files)
|
||||
const file = allFiles.find(file => file.name === fileName)
|
||||
if (file) {
|
||||
onFileMention(file.name)
|
||||
}
|
||||
}
|
||||
}, [input, onFileMention, files])
|
||||
|
||||
// Add this helper function to flatten the file tree
|
||||
// Helper function to flatten the file tree
|
||||
const getAllFiles = (items: (TFile | TFolder)[]): TFile[] => {
|
||||
return items.reduce((acc: TFile[], item) => {
|
||||
if (item.type === "file") {
|
||||
@ -204,6 +153,29 @@ export default function ChatInput({
|
||||
}, [])
|
||||
}
|
||||
|
||||
// Handle file upload from local machine via input
|
||||
const handleFileUpload = () => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = '.txt,.md,.csv,.json,.js,.ts,.html,.css,.pdf'
|
||||
input.onchange = (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0]
|
||||
if (file) {
|
||||
if (!(file.type in ALLOWED_FILE_TYPES)) {
|
||||
alert('Unsupported file type. Please upload text, code, or PDF files.')
|
||||
return
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
addContextTab("file", file.name, reader.result as string)
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex space-x-2 min-w-0">
|
||||
@ -218,6 +190,7 @@ export default function ChatInput({
|
||||
disabled={isGenerating}
|
||||
rows={1}
|
||||
/>
|
||||
{/* Render stop generation button */}
|
||||
{isGenerating ? (
|
||||
<Button
|
||||
onClick={handleStopGeneration}
|
||||
@ -238,38 +211,27 @@ export default function ChatInput({
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 w-full">
|
||||
<div className="flex items-center gap-2 px-2 text-sm text-muted-foreground min-w-auto max-w-auto">
|
||||
<Select defaultValue="claude-3.5-sonnet">
|
||||
<SelectTrigger className="h-6 w-full border-none truncate">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="claude-3.5-sonnet">claude-3.5-sonnet</SelectItem>
|
||||
<SelectItem value="claude-3">claude-3</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 sm:px-3"
|
||||
onClick={handleMentionClick}
|
||||
>
|
||||
<AtSign className="h-3 w-3 sm:mr-1" />
|
||||
<span className="hidden sm:inline">mention</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{/* Render file upload button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 sm:px-3"
|
||||
onClick={handleFileUpload}
|
||||
>
|
||||
<Paperclip className="h-3 w-3 sm:mr-1" />
|
||||
<span className="hidden sm:inline">File</span>
|
||||
</Button>
|
||||
{/* Render image upload button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 sm:px-3"
|
||||
onClick={handleImageUpload}
|
||||
>
|
||||
<ImageIcon className="h-3 w-3 sm:mr-1" />
|
||||
<span className="hidden sm:inline">Image</span>
|
||||
</Button>
|
||||
</div>
|
||||
<span className="hidden sm:inline">Image</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -1,24 +1,12 @@
|
||||
import { Check, ChevronDown, ChevronUp, Copy, CornerUpLeft } from "lucide-react"
|
||||
import { Check, Copy, CornerUpLeft } from "lucide-react"
|
||||
import React, { useState } from "react"
|
||||
import ReactMarkdown from "react-markdown"
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"
|
||||
import remarkGfm from "remark-gfm"
|
||||
import { Button } from "../../ui/button"
|
||||
import { copyToClipboard, stringifyContent } from "./lib/chatUtils"
|
||||
import ContextTabs from "./ContextTabs"
|
||||
import { Socket } from "socket.io-client"
|
||||
|
||||
interface MessageProps {
|
||||
message: {
|
||||
role: "user" | "assistant"
|
||||
content: string
|
||||
context?: string
|
||||
}
|
||||
setContext: (context: string | null) => void
|
||||
setIsContextExpanded: (isExpanded: boolean) => void
|
||||
socket: Socket | null
|
||||
}
|
||||
import { createMarkdownComponents } from './lib/markdownComponents'
|
||||
import { MessageProps } from "./types"
|
||||
|
||||
export default function ChatMessage({
|
||||
message,
|
||||
@ -26,11 +14,16 @@ export default function ChatMessage({
|
||||
setIsContextExpanded,
|
||||
socket,
|
||||
}: MessageProps) {
|
||||
|
||||
// State for expanded message index
|
||||
const [expandedMessageIndex, setExpandedMessageIndex] = useState<
|
||||
number | null
|
||||
>(null)
|
||||
|
||||
// State for copied text
|
||||
const [copiedText, setCopiedText] = useState<string | null>(null)
|
||||
|
||||
// Render copy button for text content
|
||||
const renderCopyButton = (text: any) => (
|
||||
<Button
|
||||
onClick={() => copyToClipboard(stringifyContent(text), setCopiedText)}
|
||||
@ -46,12 +39,36 @@ export default function ChatMessage({
|
||||
</Button>
|
||||
)
|
||||
|
||||
// Set context for code when asking about code
|
||||
const askAboutCode = (code: any) => {
|
||||
const contextString = stringifyContent(code)
|
||||
setContext(`Regarding this code:\n${contextString}`)
|
||||
const newContext = `Regarding this code:\n${contextString}`
|
||||
|
||||
// Format timestamp to match chat message format (HH:MM PM)
|
||||
const timestamp = new Date().toLocaleTimeString('en-US', {
|
||||
hour12: true,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
|
||||
// Instead of replacing context, append to it
|
||||
if (message.role === "assistant") {
|
||||
// For assistant messages, create a new context tab with the response content and timestamp
|
||||
setContext(newContext, `AI Response (${timestamp})`, {
|
||||
start: 1,
|
||||
end: contextString.split('\n').length
|
||||
})
|
||||
} else {
|
||||
// For user messages, create a new context tab with the selected content and timestamp
|
||||
setContext(newContext, `User Chat (${timestamp})`, {
|
||||
start: 1,
|
||||
end: contextString.split('\n').length
|
||||
})
|
||||
}
|
||||
setIsContextExpanded(false)
|
||||
}
|
||||
|
||||
// Render markdown elements for code and text
|
||||
const renderMarkdownElement = (props: any) => {
|
||||
const { node, children } = props
|
||||
const content = stringifyContent(children)
|
||||
@ -69,6 +86,7 @@ export default function ChatMessage({
|
||||
<CornerUpLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{/* Render markdown element */}
|
||||
{React.createElement(
|
||||
node.tagName,
|
||||
{
|
||||
@ -83,6 +101,13 @@ export default function ChatMessage({
|
||||
)
|
||||
}
|
||||
|
||||
// Create markdown components
|
||||
const components = createMarkdownComponents(
|
||||
renderCopyButton,
|
||||
renderMarkdownElement,
|
||||
askAboutCode
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="text-left relative">
|
||||
<div
|
||||
@ -92,6 +117,7 @@ export default function ChatMessage({
|
||||
: "bg-transparent text-white"
|
||||
} max-w-full`}
|
||||
>
|
||||
{/* Render context tabs */}
|
||||
{message.role === "user" && message.context && (
|
||||
<div className="mb-2 bg-input rounded-lg">
|
||||
<ContextTabs
|
||||
@ -111,6 +137,7 @@ export default function ChatMessage({
|
||||
message.context.replace(/^Regarding this code:\n/, "")
|
||||
)}
|
||||
</div>
|
||||
{/* Render code textarea */}
|
||||
{(() => {
|
||||
const code = message.context.replace(
|
||||
/^Regarding this code:\n/,
|
||||
@ -124,7 +151,10 @@ export default function ChatMessage({
|
||||
value={code}
|
||||
onChange={(e) => {
|
||||
const updatedContext = `Regarding this code:\n${e.target.value}`
|
||||
setContext(updatedContext)
|
||||
setContext(updatedContext, "Selected Content", {
|
||||
start: 1,
|
||||
end: e.target.value.split('\n').length
|
||||
})
|
||||
}}
|
||||
className="w-full p-2 bg-[#1e1e1e] text-white font-mono text-sm rounded"
|
||||
rows={code.split("\n").length}
|
||||
@ -141,8 +171,9 @@ export default function ChatMessage({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Render copy and ask about code buttons */}
|
||||
{message.role === "user" && (
|
||||
<div className="absolute top-0 right-0 flex opacity-0 group-hover:opacity-30 transition-opacity">
|
||||
<div className="absolute top-0 right-0 p-1 flex opacity-40">
|
||||
{renderCopyButton(message.content)}
|
||||
<Button
|
||||
onClick={() => askAboutCode(message.content)}
|
||||
@ -154,67 +185,11 @@ export default function ChatMessage({
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{/* Render markdown content */}
|
||||
{message.role === "assistant" ? (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
code({ node, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || "")
|
||||
return match ? (
|
||||
<div className="relative border border-input rounded-md my-4">
|
||||
<div className="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 bg-#1e1e1e rounded-tl">
|
||||
{match[1]}
|
||||
</div>
|
||||
<div className="absolute top-0 right-0 flex">
|
||||
{renderCopyButton(children)}
|
||||
<Button
|
||||
onClick={() => askAboutCode(children)}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="p-1 h-6"
|
||||
>
|
||||
<CornerUpLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="pt-6">
|
||||
<SyntaxHighlighter
|
||||
style={vscDarkPlus as any}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: "0.5rem",
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
>
|
||||
{stringifyContent(children)}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
},
|
||||
p: renderMarkdownElement,
|
||||
h1: renderMarkdownElement,
|
||||
h2: renderMarkdownElement,
|
||||
h3: renderMarkdownElement,
|
||||
h4: renderMarkdownElement,
|
||||
h5: renderMarkdownElement,
|
||||
h6: renderMarkdownElement,
|
||||
ul: (props) => (
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
{props.children}
|
||||
</ul>
|
||||
),
|
||||
ol: (props) => (
|
||||
<ol className="list-decimal pl-6 mb-4 space-y-2">
|
||||
{props.children}
|
||||
</ol>
|
||||
),
|
||||
}}
|
||||
components={components}
|
||||
>
|
||||
{message.content}
|
||||
</ReactMarkdown>
|
||||
@ -226,6 +201,7 @@ export default function ChatMessage({
|
||||
)
|
||||
}
|
||||
|
||||
// Parse context to tabs for context tabs component
|
||||
function parseContextToTabs(context: string) {
|
||||
const sections = context.split(/(?=File |Code from )/)
|
||||
return sections.map((section, index) => {
|
||||
@ -236,6 +212,7 @@ function parseContextToTabs(context: string) {
|
||||
// Remove code block markers for display
|
||||
content = content.replace(/^```[\w-]*\n/, '').replace(/\n```$/, '')
|
||||
|
||||
// Determine if the context is a file or code
|
||||
const isFile = titleLine.startsWith('File ')
|
||||
const name = titleLine.replace(/^(File |Code from )/, '').replace(':', '')
|
||||
|
||||
|
@ -1,60 +0,0 @@
|
||||
import { ChevronDown, ChevronUp, X } from "lucide-react"
|
||||
|
||||
interface ContextDisplayProps {
|
||||
context: string | null
|
||||
isContextExpanded: boolean
|
||||
setIsContextExpanded: (isExpanded: boolean) => void
|
||||
setContext: (context: string | null) => void
|
||||
}
|
||||
|
||||
export default function ContextDisplay({
|
||||
context,
|
||||
isContextExpanded,
|
||||
setIsContextExpanded,
|
||||
setContext,
|
||||
}: ContextDisplayProps) {
|
||||
if (!context) return null
|
||||
|
||||
return (
|
||||
<div className="mb-2 bg-input p-2 rounded-lg">
|
||||
<div className="flex justify-between items-center">
|
||||
<div
|
||||
className="flex-grow cursor-pointer"
|
||||
onClick={() => setIsContextExpanded(!isContextExpanded)}
|
||||
>
|
||||
<span className="text-sm text-gray-300">Context</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
{isContextExpanded ? (
|
||||
<ChevronUp
|
||||
size={16}
|
||||
className="cursor-pointer"
|
||||
onClick={() => setIsContextExpanded(false)}
|
||||
/>
|
||||
) : (
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className="cursor-pointer"
|
||||
onClick={() => setIsContextExpanded(true)}
|
||||
/>
|
||||
)}
|
||||
<X
|
||||
size={16}
|
||||
className="ml-2 cursor-pointer text-gray-400 hover:text-gray-200"
|
||||
onClick={() => setContext(null)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isContextExpanded && (
|
||||
<textarea
|
||||
value={context.replace(/^Regarding this code:\n/, "")}
|
||||
onChange={(e) =>
|
||||
setContext(`Regarding this code:\n${e.target.value}`)
|
||||
}
|
||||
className="w-full mt-2 p-2 bg-#1e1e1e text-white rounded"
|
||||
rows={5}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { Plus, X, ChevronDown, ChevronUp, Image as ImageIcon, FileText } from "lucide-react"
|
||||
import { Plus, X, Image as ImageIcon, FileText } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { Button } from "../../ui/button"
|
||||
import { TFile, TFolder } from "@/lib/types"
|
||||
@ -8,44 +8,28 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Socket } from "socket.io-client"
|
||||
|
||||
interface ContextTab {
|
||||
id: string
|
||||
type: "file" | "code" | "image"
|
||||
name: string
|
||||
content: string
|
||||
sourceFile?: string
|
||||
lineRange?: {
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
}
|
||||
|
||||
interface ContextTabsProps {
|
||||
activeFileName: string
|
||||
onAddFile: () => void
|
||||
contextTabs: ContextTab[]
|
||||
onRemoveTab: (id: string) => void
|
||||
isExpanded: boolean
|
||||
onToggleExpand: () => void
|
||||
files?: (TFile | TFolder)[]
|
||||
onFileSelect?: (file: TFile) => void
|
||||
socket: Socket | null
|
||||
}
|
||||
import { ContextTab } from "./types"
|
||||
import { ContextTabsProps } from "./types"
|
||||
|
||||
export default function ContextTabs({
|
||||
onAddFile,
|
||||
contextTabs,
|
||||
onRemoveTab,
|
||||
className,
|
||||
files = [],
|
||||
onFileSelect,
|
||||
}: ContextTabsProps & { className?: string }) {
|
||||
|
||||
// State for preview tab
|
||||
const [previewTab, setPreviewTab] = useState<ContextTab | null>(null)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
|
||||
// Allow preview for images and code selections from editor
|
||||
const togglePreview = (tab: ContextTab) => {
|
||||
if (!tab.lineRange && tab.type !== "image") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle preview for images and code selections from editor
|
||||
if (previewTab?.id === tab.id) {
|
||||
setPreviewTab(null)
|
||||
} else {
|
||||
@ -53,6 +37,7 @@ export default function ContextTabs({
|
||||
}
|
||||
}
|
||||
|
||||
// Remove tab from context when clicking on X
|
||||
const handleRemoveTab = (id: string) => {
|
||||
if (previewTab?.id === id) {
|
||||
setPreviewTab(null)
|
||||
@ -60,6 +45,7 @@ export default function ContextTabs({
|
||||
onRemoveTab(id)
|
||||
}
|
||||
|
||||
// Get all files from the file tree to search for context
|
||||
const getAllFiles = (items: (TFile | TFolder)[]): TFile[] => {
|
||||
return items.reduce((acc: TFile[], item) => {
|
||||
if (item.type === "file") {
|
||||
@ -71,6 +57,7 @@ export default function ContextTabs({
|
||||
}, [])
|
||||
}
|
||||
|
||||
// Get all files from the file tree to search for context when adding context
|
||||
const allFiles = getAllFiles(files)
|
||||
const filteredFiles = allFiles.filter(file =>
|
||||
file.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
@ -80,6 +67,7 @@ export default function ContextTabs({
|
||||
<div className={`border-none ${className || ''}`}>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-1 overflow-hidden mb-2 flex-wrap">
|
||||
{/* Add context tab button */}
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
@ -90,13 +78,16 @@ export default function ContextTabs({
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
{/* Add context tab popover */}
|
||||
<PopoverContent className="w-64 p-2">
|
||||
<Input
|
||||
placeholder="Search files..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="mb-2"
|
||||
/>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<Input
|
||||
placeholder="Search files..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-[200px] overflow-y-auto">
|
||||
{filteredFiles.map((file) => (
|
||||
<Button
|
||||
@ -112,11 +103,13 @@ export default function ContextTabs({
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{/* Add context tab button */}
|
||||
{contextTabs.length === 0 && (
|
||||
<div className="flex items-center gap-1 px-2 rounded">
|
||||
<span className="text-sm text-muted-foreground">Add Context</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Render context tabs */}
|
||||
{contextTabs.map((tab) => (
|
||||
<div
|
||||
key={tab.id}
|
||||
@ -143,18 +136,24 @@ export default function ContextTabs({
|
||||
{/* Preview Section */}
|
||||
{previewTab && (
|
||||
<div className="p-2 bg-input rounded-md max-h-[200px] overflow-auto mb-2">
|
||||
{previewTab.lineRange && (
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
Lines {previewTab.lineRange.start}-{previewTab.lineRange.end}
|
||||
</div>
|
||||
)}
|
||||
{previewTab.type === "image" ? (
|
||||
<img
|
||||
src={previewTab.content}
|
||||
alt={previewTab.name}
|
||||
className="max-w-full h-auto"
|
||||
/>
|
||||
) : (
|
||||
) : previewTab.lineRange && (
|
||||
<>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
Lines {previewTab.lineRange.start}-{previewTab.lineRange.end}
|
||||
</div>
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap">
|
||||
{previewTab.content}
|
||||
</pre>
|
||||
</>
|
||||
)}
|
||||
{/* Render file context tab */}
|
||||
{previewTab.type === "file" && (
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap">
|
||||
{previewTab.content}
|
||||
</pre>
|
||||
|
@ -6,32 +6,9 @@ import ChatMessage from "./ChatMessage"
|
||||
import ContextTabs from "./ContextTabs"
|
||||
import { handleSend, handleStopGeneration } from "./lib/chatUtils"
|
||||
import { nanoid } from 'nanoid'
|
||||
import * as monaco from 'monaco-editor'
|
||||
import { TFile, TFolder } from "@/lib/types"
|
||||
import { TFile } from "@/lib/types"
|
||||
import { useSocket } from "@/context/SocketContext"
|
||||
|
||||
interface Message {
|
||||
role: "user" | "assistant"
|
||||
content: string
|
||||
context?: string
|
||||
}
|
||||
|
||||
interface ContextTab {
|
||||
id: string
|
||||
type: "file" | "code" | "image"
|
||||
name: string
|
||||
content: string
|
||||
lineRange?: { start: number; end: number }
|
||||
}
|
||||
|
||||
interface AIChatProps {
|
||||
activeFileContent: string
|
||||
activeFileName: string
|
||||
onClose: () => void
|
||||
editorRef: React.MutableRefObject<monaco.editor.IStandaloneCodeEditor | undefined>
|
||||
lastCopiedRangeRef: React.MutableRefObject<{ startLine: number; endLine: number } | null>
|
||||
files: (TFile | TFolder)[]
|
||||
}
|
||||
import { Message, ContextTab, AIChatProps } from './types'
|
||||
|
||||
export default function AIChat({
|
||||
activeFileContent,
|
||||
@ -41,21 +18,32 @@ export default function AIChat({
|
||||
lastCopiedRangeRef,
|
||||
files,
|
||||
}: AIChatProps) {
|
||||
// Initialize socket and messages
|
||||
const { socket } = useSocket()
|
||||
const [messages, setMessages] = useState<Message[]>([])
|
||||
|
||||
// Initialize input and state for generating messages
|
||||
const [input, setInput] = useState("")
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
|
||||
// Initialize chat container ref and abort controller ref
|
||||
const chatContainerRef = useRef<HTMLDivElement>(null)
|
||||
const abortControllerRef = useRef<AbortController | null>(null)
|
||||
|
||||
// Initialize context tabs and state for expanding context
|
||||
const [contextTabs, setContextTabs] = useState<ContextTab[]>([])
|
||||
const [isContextExpanded, setIsContextExpanded] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// Initialize textarea ref
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
// Scroll to bottom of chat when messages change
|
||||
useEffect(() => {
|
||||
scrollToBottom()
|
||||
}, [messages])
|
||||
|
||||
// Scroll to bottom of chat when messages change
|
||||
const scrollToBottom = () => {
|
||||
if (chatContainerRef.current) {
|
||||
setTimeout(() => {
|
||||
@ -67,6 +55,7 @@ export default function AIChat({
|
||||
}
|
||||
}
|
||||
|
||||
// Add context tab to context tabs
|
||||
const addContextTab = (type: string, name: string, content: string, lineRange?: { start: number; end: number }) => {
|
||||
const newTab = {
|
||||
id: nanoid(),
|
||||
@ -78,19 +67,22 @@ export default function AIChat({
|
||||
setContextTabs(prev => [...prev, newTab])
|
||||
}
|
||||
|
||||
// Remove context tab from context tabs
|
||||
const removeContextTab = (id: string) => {
|
||||
setContextTabs(prev => prev.filter(tab => tab.id !== id))
|
||||
}
|
||||
|
||||
const handleAddFile = () => {
|
||||
console.log("Add file to context")
|
||||
// Add file to context tabs
|
||||
const handleAddFile = (tab: ContextTab) => {
|
||||
setContextTabs(prev => [...prev, tab])
|
||||
}
|
||||
|
||||
// Format code content to remove starting and ending code block markers if they exist
|
||||
const formatCodeContent = (content: string) => {
|
||||
// Remove starting and ending code block markers if they exist
|
||||
return content.replace(/^```[\w-]*\n/, '').replace(/\n```$/, '')
|
||||
}
|
||||
|
||||
// Get combined context from context tabs
|
||||
const getCombinedContext = () => {
|
||||
if (contextTabs.length === 0) return ''
|
||||
|
||||
@ -107,6 +99,7 @@ export default function AIChat({
|
||||
}).join('\n\n')
|
||||
}
|
||||
|
||||
// Handle sending message with context
|
||||
const handleSendWithContext = () => {
|
||||
const combinedContext = getCombinedContext()
|
||||
handleSend(
|
||||
@ -125,51 +118,21 @@ export default function AIChat({
|
||||
setContextTabs([])
|
||||
}
|
||||
|
||||
function setContext(context: string | null, fileName?: string, lineRange?: { start: number; end: number }): void {
|
||||
// Set context for the chat
|
||||
const setContext = (
|
||||
context: string | null,
|
||||
name: string,
|
||||
range?: { start: number, end: number }
|
||||
) => {
|
||||
if (!context) {
|
||||
setContextTabs([])
|
||||
return
|
||||
}
|
||||
|
||||
const existingCodeTab = contextTabs.find(tab => tab.type === 'code')
|
||||
|
||||
if (existingCodeTab) {
|
||||
setContextTabs(prev =>
|
||||
prev.map(tab =>
|
||||
tab.id === existingCodeTab.id
|
||||
? { ...tab, content: context, name: fileName || 'Code Context', lineRange }
|
||||
: tab
|
||||
)
|
||||
)
|
||||
} else {
|
||||
addContextTab('code', fileName || 'Chat Context', context, lineRange)
|
||||
}
|
||||
// Always add a new tab instead of updating existing ones
|
||||
addContextTab('code', name, context, range)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (editorRef?.current) {
|
||||
const editor = editorRef.current;
|
||||
|
||||
// Configure editor options for better copy handling
|
||||
editor.updateOptions({
|
||||
copyWithSyntaxHighlighting: true,
|
||||
emptySelectionClipboard: false
|
||||
});
|
||||
|
||||
// Track selection changes
|
||||
const disposable = editor.onDidChangeCursorSelection((e) => {
|
||||
if (!e.selection.isEmpty()) {
|
||||
lastCopiedRangeRef.current = {
|
||||
startLine: e.selection.startLineNumber,
|
||||
endLine: e.selection.endLineNumber
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return () => disposable.dispose();
|
||||
}
|
||||
}, [editorRef?.current]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen w-full">
|
||||
<div className="flex justify-between items-center p-2 border-b">
|
||||
@ -193,6 +156,7 @@ export default function AIChat({
|
||||
className="flex-grow overflow-y-auto p-4 space-y-4"
|
||||
>
|
||||
{messages.map((message, messageIndex) => (
|
||||
// Render chat message component for each message
|
||||
<ChatMessage
|
||||
key={messageIndex}
|
||||
message={message}
|
||||
@ -204,6 +168,7 @@ export default function AIChat({
|
||||
{isLoading && <LoadingDots />}
|
||||
</div>
|
||||
<div className="p-4 border-t mb-14">
|
||||
{/* Render context tabs component */}
|
||||
<ContextTabs
|
||||
activeFileName={activeFileName}
|
||||
onAddFile={handleAddFile}
|
||||
@ -224,9 +189,9 @@ export default function AIChat({
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{/* Render chat input component */}
|
||||
<ChatInput
|
||||
textareaRef={textareaRef}
|
||||
files={[]}
|
||||
addContextTab={addContextTab}
|
||||
editorRef={editorRef}
|
||||
input={input}
|
||||
@ -243,13 +208,11 @@ export default function AIChat({
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}}
|
||||
onFileMention={(fileName) => {
|
||||
}}
|
||||
lastCopiedRangeRef={lastCopiedRangeRef}
|
||||
activeFileName={activeFileName}
|
||||
contextTabs={contextTabs.map(tab => ({
|
||||
...tab,
|
||||
title: tab.id // Add missing title property
|
||||
title: tab.id
|
||||
}))}
|
||||
onRemoveTab={removeContextTab}
|
||||
/>
|
||||
|
@ -1,30 +1,39 @@
|
||||
import React from "react"
|
||||
|
||||
// Stringify content for chat message component
|
||||
export const stringifyContent = (
|
||||
content: any,
|
||||
seen = new WeakSet()
|
||||
): string => {
|
||||
// Stringify content if it's a string
|
||||
if (typeof content === "string") {
|
||||
return content
|
||||
}
|
||||
// Stringify content if it's null
|
||||
if (content === null) {
|
||||
return "null"
|
||||
}
|
||||
// Stringify content if it's undefined
|
||||
if (content === undefined) {
|
||||
return "undefined"
|
||||
}
|
||||
// Stringify content if it's a number or boolean
|
||||
if (typeof content === "number" || typeof content === "boolean") {
|
||||
return content.toString()
|
||||
}
|
||||
// Stringify content if it's a function
|
||||
if (typeof content === "function") {
|
||||
return content.toString()
|
||||
}
|
||||
// Stringify content if it's a symbol
|
||||
if (typeof content === "symbol") {
|
||||
return content.toString()
|
||||
}
|
||||
// Stringify content if it's a bigint
|
||||
if (typeof content === "bigint") {
|
||||
return content.toString() + "n"
|
||||
}
|
||||
// Stringify content if it's a valid React element
|
||||
if (React.isValidElement(content)) {
|
||||
return React.Children.toArray(
|
||||
(content as React.ReactElement).props.children
|
||||
@ -32,11 +41,13 @@ export const stringifyContent = (
|
||||
.map((child) => stringifyContent(child, seen))
|
||||
.join("")
|
||||
}
|
||||
// Stringify content if it's an array
|
||||
if (Array.isArray(content)) {
|
||||
return (
|
||||
"[" + content.map((item) => stringifyContent(item, seen)).join(", ") + "]"
|
||||
)
|
||||
}
|
||||
// Stringify content if it's an object
|
||||
if (typeof content === "object") {
|
||||
if (seen.has(content)) {
|
||||
return "[Circular]"
|
||||
@ -51,19 +62,23 @@ export const stringifyContent = (
|
||||
return Object.prototype.toString.call(content)
|
||||
}
|
||||
}
|
||||
// Stringify content if it's a primitive value
|
||||
return String(content)
|
||||
}
|
||||
|
||||
// Copy to clipboard for chat message component
|
||||
export const copyToClipboard = (
|
||||
text: string,
|
||||
setCopiedText: (text: string | null) => void
|
||||
) => {
|
||||
// Copy text to clipboard for chat message component
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
setCopiedText(text)
|
||||
setTimeout(() => setCopiedText(null), 2000)
|
||||
})
|
||||
}
|
||||
|
||||
// Handle send for chat message component
|
||||
export const handleSend = async (
|
||||
input: string,
|
||||
context: string | null,
|
||||
@ -76,14 +91,26 @@ export const handleSend = async (
|
||||
abortControllerRef: React.MutableRefObject<AbortController | null>,
|
||||
activeFileContent: string
|
||||
) => {
|
||||
if (input.trim() === "" && !context) return
|
||||
// Return if input is empty and context is null
|
||||
if (input.trim() === "" && !context) return
|
||||
|
||||
const newMessage = {
|
||||
// Get timestamp for chat message component
|
||||
const timestamp = new Date().toLocaleTimeString('en-US', {
|
||||
hour12: true,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}).replace(/(\d{2}):(\d{2})/, '$1:$2')
|
||||
|
||||
// Create user message for chat message component
|
||||
const userMessage = {
|
||||
role: "user" as const,
|
||||
content: input,
|
||||
context: context || undefined,
|
||||
timestamp: timestamp
|
||||
}
|
||||
const updatedMessages = [...messages, newMessage]
|
||||
|
||||
// Update messages for chat message component
|
||||
const updatedMessages = [...messages, userMessage]
|
||||
setMessages(updatedMessages)
|
||||
setInput("")
|
||||
setIsContextExpanded(false)
|
||||
@ -93,11 +120,13 @@ export const handleSend = async (
|
||||
abortControllerRef.current = new AbortController()
|
||||
|
||||
try {
|
||||
// Create anthropic messages for chat message component
|
||||
const anthropicMessages = updatedMessages.map((msg) => ({
|
||||
role: msg.role === "user" ? "human" : "assistant",
|
||||
content: msg.content,
|
||||
}))
|
||||
|
||||
// Fetch AI response for chat message component
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_AI_WORKER_URL}/api`,
|
||||
{
|
||||
@ -114,20 +143,24 @@ export const handleSend = async (
|
||||
}
|
||||
)
|
||||
|
||||
// Throw error if response is not ok
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to get AI response")
|
||||
}
|
||||
|
||||
// Get reader for chat message component
|
||||
const reader = response.body?.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
const assistantMessage = { role: "assistant" as const, content: "" }
|
||||
setMessages([...updatedMessages, assistantMessage])
|
||||
setIsLoading(false)
|
||||
|
||||
// Initialize buffer for chat message component
|
||||
let buffer = ""
|
||||
const updateInterval = 100
|
||||
let lastUpdateTime = Date.now()
|
||||
|
||||
// Read response from reader for chat message component
|
||||
if (reader) {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
@ -146,6 +179,7 @@ export const handleSend = async (
|
||||
}
|
||||
}
|
||||
|
||||
// Update messages for chat message component
|
||||
setMessages((prev) => {
|
||||
const updatedMessages = [...prev]
|
||||
const lastMessage = updatedMessages[updatedMessages.length - 1]
|
||||
@ -154,6 +188,7 @@ export const handleSend = async (
|
||||
})
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Handle abort error for chat message component
|
||||
if (error.name === "AbortError") {
|
||||
console.log("Generation aborted")
|
||||
} else {
|
||||
@ -171,6 +206,7 @@ export const handleSend = async (
|
||||
}
|
||||
}
|
||||
|
||||
// Handle stop generation for chat message component
|
||||
export const handleStopGeneration = (
|
||||
abortControllerRef: React.MutableRefObject<AbortController | null>
|
||||
) => {
|
||||
@ -178,3 +214,22 @@ export const handleStopGeneration = (
|
||||
abortControllerRef.current.abort()
|
||||
}
|
||||
}
|
||||
|
||||
// Check if text looks like code for chat message component
|
||||
export const looksLikeCode = (text: string): boolean => {
|
||||
const codeIndicators = [
|
||||
/^import\s+/m, // import statements
|
||||
/^function\s+/m, // function declarations
|
||||
/^class\s+/m, // class declarations
|
||||
/^const\s+/m, // const declarations
|
||||
/^let\s+/m, // let declarations
|
||||
/^var\s+/m, // var declarations
|
||||
/[{}\[\]();]/, // common code syntax
|
||||
/^\s*\/\//m, // comments
|
||||
/^\s*\/\*/m, // multi-line comments
|
||||
/=>/, // arrow functions
|
||||
/^export\s+/m, // export statements
|
||||
];
|
||||
|
||||
return codeIndicators.some(pattern => pattern.test(text));
|
||||
};
|
||||
|
79
frontend/components/editor/AIChat/lib/markdownComponents.tsx
Normal file
79
frontend/components/editor/AIChat/lib/markdownComponents.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import { Components } from "react-markdown"
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"
|
||||
import { Button } from "../../../ui/button"
|
||||
import { CornerUpLeft } from "lucide-react"
|
||||
import { stringifyContent } from "./chatUtils"
|
||||
|
||||
// Create markdown components for chat message component
|
||||
export const createMarkdownComponents = (
|
||||
renderCopyButton: (text: any) => JSX.Element,
|
||||
renderMarkdownElement: (props: any) => JSX.Element,
|
||||
askAboutCode: (code: any) => void
|
||||
): Components => ({
|
||||
code: ({ node, className, children, ...props }: {
|
||||
node?: import('hast').Element,
|
||||
className?: string,
|
||||
children?: React.ReactNode,
|
||||
[key: string]: any,
|
||||
}) => {
|
||||
const match = /language-(\w+)/.exec(className || "")
|
||||
|
||||
return match ? (
|
||||
<div className="relative border border-input rounded-md my-4">
|
||||
<div className="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 bg-#1e1e1e rounded-tl">
|
||||
{match[1]}
|
||||
</div>
|
||||
<div className="absolute top-0 right-0 flex">
|
||||
{renderCopyButton(children)}
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
askAboutCode(children)
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="p-1 h-6"
|
||||
>
|
||||
<CornerUpLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="pt-6">
|
||||
<SyntaxHighlighter
|
||||
style={vscDarkPlus as any}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: "0.5rem",
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
>
|
||||
{stringifyContent(children)}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<code className={className} {...props}>{children}</code>
|
||||
)
|
||||
},
|
||||
// Render markdown elements
|
||||
p: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||
h1: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||
h2: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||
h3: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||
h4: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||
h5: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||
h6: ({ node, children, ...props }) => renderMarkdownElement({ node, children, ...props }),
|
||||
ul: (props) => (
|
||||
<ul className="list-disc pl-6 mb-4 space-y-2">
|
||||
{props.children}
|
||||
</ul>
|
||||
),
|
||||
ol: (props) => (
|
||||
<ol className="list-decimal pl-6 mb-4 space-y-2">
|
||||
{props.children}
|
||||
</ol>
|
||||
),
|
||||
})
|
93
frontend/components/editor/AIChat/types/index.ts
Normal file
93
frontend/components/editor/AIChat/types/index.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import * as monaco from 'monaco-editor'
|
||||
import { TFile, TFolder } from "@/lib/types"
|
||||
import { Socket } from 'socket.io-client';
|
||||
|
||||
// Allowed file types for context tabs
|
||||
export const ALLOWED_FILE_TYPES = {
|
||||
// Text files
|
||||
'text/plain': true,
|
||||
'text/markdown': true,
|
||||
'text/csv': true,
|
||||
// Code files
|
||||
'application/json': true,
|
||||
'text/javascript': true,
|
||||
'text/typescript': true,
|
||||
'text/html': true,
|
||||
'text/css': true,
|
||||
// Documents
|
||||
'application/pdf': true,
|
||||
// Images
|
||||
'image/jpeg': true,
|
||||
'image/png': true,
|
||||
'image/gif': true,
|
||||
'image/webp': true,
|
||||
'image/svg+xml': true,
|
||||
} as const;
|
||||
|
||||
// Message interface
|
||||
export interface Message {
|
||||
role: "user" | "assistant"
|
||||
content: string
|
||||
context?: string
|
||||
}
|
||||
|
||||
// Context tab interface
|
||||
export interface ContextTab {
|
||||
id: string
|
||||
type: "file" | "code" | "image"
|
||||
name: string
|
||||
content: string
|
||||
lineRange?: { start: number; end: number }
|
||||
}
|
||||
|
||||
// AIChat props interface
|
||||
export interface AIChatProps {
|
||||
activeFileContent: string
|
||||
activeFileName: string
|
||||
onClose: () => void
|
||||
editorRef: React.MutableRefObject<monaco.editor.IStandaloneCodeEditor | undefined>
|
||||
lastCopiedRangeRef: React.MutableRefObject<{ startLine: number; endLine: number } | null>
|
||||
files: (TFile | TFolder)[]
|
||||
}
|
||||
|
||||
// Chat input props interface
|
||||
export interface ChatInputProps {
|
||||
input: string
|
||||
setInput: (input: string) => void
|
||||
isGenerating: boolean
|
||||
handleSend: (useFullContext?: boolean) => void
|
||||
handleStopGeneration: () => void
|
||||
onImageUpload: (file: File) => void
|
||||
addContextTab: (type: string, title: string, content: string, lineRange?: { start: number, end: number }) => void
|
||||
activeFileName?: string
|
||||
editorRef: React.MutableRefObject<monaco.editor.IStandaloneCodeEditor | undefined>
|
||||
lastCopiedRangeRef: React.MutableRefObject<{ startLine: number; endLine: number } | null>
|
||||
contextTabs: { id: string; type: string; title: string; content: string; lineRange?: { start: number; end: number } }[]
|
||||
onRemoveTab: (id: string) => void
|
||||
textareaRef: React.RefObject<HTMLTextAreaElement>
|
||||
}
|
||||
|
||||
// Chat message props interface
|
||||
export interface MessageProps {
|
||||
message: {
|
||||
role: "user" | "assistant"
|
||||
content: string
|
||||
context?: string
|
||||
}
|
||||
setContext: (context: string | null, name: string, range?: { start: number, end: number }) => void
|
||||
setIsContextExpanded: (isExpanded: boolean) => void
|
||||
socket: Socket | null
|
||||
}
|
||||
|
||||
// Context tabs props interface
|
||||
export interface ContextTabsProps {
|
||||
activeFileName: string
|
||||
onAddFile: (tab: ContextTab) => void
|
||||
contextTabs: ContextTab[]
|
||||
onRemoveTab: (id: string) => void
|
||||
isExpanded: boolean
|
||||
onToggleExpand: () => void
|
||||
files?: (TFile | TFolder)[]
|
||||
onFileSelect?: (file: TFile) => void
|
||||
socket: Socket | null
|
||||
}
|
1669
package-lock.json
generated
1669
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@radix-ui/react-popover": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.15"
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user