253 lines
8.2 KiB
TypeScript
Raw Normal View History

import { Check, Copy, CornerUpLeft } from "lucide-react"
2024-10-21 13:57:17 -06:00
import React, { useState } from "react"
import ReactMarkdown from "react-markdown"
import remarkGfm from "remark-gfm"
import { Button } from "../../ui/button"
import ContextTabs from "./ContextTabs"
2024-11-17 12:35:56 -05:00
import { copyToClipboard, stringifyContent } from "./lib/chatUtils"
import { createMarkdownComponents } from "./lib/markdownComponents"
import { MessageProps } from "./types"
2024-10-21 13:57:17 -06:00
export default function ChatMessage({
message,
setContext,
setIsContextExpanded,
socket,
handleApplyCode,
activeFileName,
activeFileContent,
editorRef,
mergeDecorationsCollection,
setMergeDecorationsCollection,
2024-10-21 13:57:17 -06:00
}: MessageProps) {
// State for expanded message index
2024-10-21 13:57:17 -06:00
const [expandedMessageIndex, setExpandedMessageIndex] = useState<
number | null
>(null)
// State for copied text
2024-10-21 13:57:17 -06:00
const [copiedText, setCopiedText] = useState<string | null>(null)
2024-11-17 12:35:56 -05:00
// Render copy button for text content
const renderCopyButton = (text: any) => (
<Button
onClick={() => copyToClipboard(stringifyContent(text), setCopiedText)}
size="sm"
variant="ghost"
className="p-1 h-6"
>
{copiedText === stringifyContent(text) ? (
<Check className="w-4 h-4 text-green-500" />
) : (
<Copy className="w-4 h-4" />
)}
</Button>
2024-10-21 13:57:17 -06:00
)
// Set context for code when asking about code
const askAboutCode = (code: any) => {
2024-10-21 13:57:17 -06:00
const contextString = stringifyContent(code)
const newContext = `Regarding this code:\n${contextString}`
2024-11-17 12:35:56 -05:00
// Format timestamp to match chat message format (HH:MM PM)
2024-11-17 12:35:56 -05:00
const timestamp = new Date().toLocaleTimeString("en-US", {
hour12: true,
2024-11-17 12:35:56 -05:00
hour: "2-digit",
minute: "2-digit",
})
2024-11-17 12:35:56 -05:00
// 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,
2024-11-17 12:35:56 -05:00
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,
2024-11-17 12:35:56 -05:00
end: contextString.split("\n").length,
})
}
2024-10-21 13:57:17 -06:00
setIsContextExpanded(false)
}
// Render markdown elements for code and text
const renderMarkdownElement = (props: any) => {
2024-10-21 13:57:17 -06:00
const { node, children } = props
const content = stringifyContent(children)
return (
<div className="relative group">
<div className="absolute top-0 right-0 flex opacity-0 group-hover:opacity-30 transition-opacity">
{renderCopyButton(content)}
<Button
onClick={() => askAboutCode(content)}
size="sm"
variant="ghost"
className="p-1 h-6"
>
<CornerUpLeft className="w-4 h-4" />
</Button>
</div>
{/* Render markdown element */}
2024-10-21 13:57:17 -06:00
{React.createElement(
node.tagName,
{
...props,
className: `${
props.className || ""
} hover:bg-transparent rounded p-1 transition-colors`,
},
children
)}
</div>
2024-10-21 13:57:17 -06:00
)
}
// Create markdown components
const components = createMarkdownComponents(
renderCopyButton,
renderMarkdownElement,
askAboutCode,
activeFileName,
activeFileContent,
editorRef,
handleApplyCode,
mergeDecorationsCollection,
setMergeDecorationsCollection
)
return (
<div className="text-left relative">
2024-10-21 13:57:17 -06:00
<div
className={`relative p-2 rounded-lg ${
message.role === "user"
? "bg-[#262626] text-white"
: "bg-transparent text-white"
} max-w-full`}
>
{/* Render context tabs */}
{message.role === "user" && message.context && (
<div className="mb-2 bg-input rounded-lg">
<ContextTabs
socket={socket}
activeFileName=""
onAddFile={() => {}}
contextTabs={parseContextToTabs(message.context)}
onRemoveTab={() => {}}
isExpanded={expandedMessageIndex === 0}
2024-11-17 12:35:56 -05:00
onToggleExpand={() =>
setExpandedMessageIndex(expandedMessageIndex === 0 ? null : 0)
}
className="[&_div:first-child>div:first-child>div]:bg-[#0D0D0D] [&_button:first-child]:hidden [&_button:last-child]:hidden"
/>
{expandedMessageIndex === 0 && (
<div className="relative">
<div className="absolute top-0 right-0 flex p-1">
2024-10-21 13:57:17 -06:00
{renderCopyButton(
message.context.replace(/^Regarding this code:\n/, "")
)}
</div>
{/* Render code textarea */}
{(() => {
2024-10-21 13:57:17 -06:00
const code = message.context.replace(
/^Regarding this code:\n/,
""
)
const match = /language-(\w+)/.exec(code)
const language = match ? match[1] : "typescript"
return (
<div className="pt-6">
<textarea
value={code}
onChange={(e) => {
2024-10-21 13:57:17 -06:00
const updatedContext = `Regarding this code:\n${e.target.value}`
setContext(updatedContext, "Selected Content", {
start: 1,
2024-11-17 12:35:56 -05:00
end: e.target.value.split("\n").length,
})
}}
className="w-full p-2 bg-[#1e1e1e] text-white font-mono text-sm rounded"
2024-10-21 13:57:17 -06:00
rows={code.split("\n").length}
style={{
2024-10-21 13:57:17 -06:00
resize: "vertical",
minHeight: "100px",
maxHeight: "400px",
}}
/>
</div>
2024-10-21 13:57:17 -06:00
)
})()}
</div>
)}
</div>
)}
{/* Render copy and ask about code buttons */}
{message.role === "user" && (
<div className="absolute top-0 right-0 p-1 flex opacity-40">
{renderCopyButton(message.content)}
<Button
onClick={() => askAboutCode(message.content)}
size="sm"
variant="ghost"
className="p-1 h-6"
>
<CornerUpLeft className="w-4 h-4" />
</Button>
</div>
)}
{/* Render markdown content */}
2024-10-21 13:57:17 -06:00
{message.role === "assistant" ? (
2024-11-17 12:35:56 -05:00
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
{message.content}
</ReactMarkdown>
) : (
2024-10-21 13:57:17 -06:00
<div className="whitespace-pre-wrap group">{message.content}</div>
)}
</div>
</div>
2024-10-21 13:57:17 -06:00
)
}
2024-11-17 12:35:56 -05:00
// Parse context to tabs for context tabs component
function parseContextToTabs(context: string) {
2024-11-29 21:49:44 -05:00
// Use specific regex patterns to avoid matching import statements
const sections = context.split(/(?=File |Code from |Image \d{1,2}:)/)
2024-11-17 12:35:56 -05:00
return sections
.map((section, index) => {
const lines = section.trim().split("\n")
const titleLine = lines[0]
let content = lines.slice(1).join("\n").trim()
// Remove code block markers for display
content = content.replace(/^```[\w-]*\n/, "").replace(/\n```$/, "")
2024-11-29 21:49:44 -05:00
// Determine the type of context
2024-11-17 12:35:56 -05:00
const isFile = titleLine.startsWith("File ")
2024-11-29 21:49:44 -05:00
const isImage = titleLine.startsWith("Image ")
const type = isFile ? "file" : isImage ? "image" : "code"
const name = titleLine
.replace(/^(File |Code from |Image )/, "")
.replace(":", "")
.trim()
// Skip if the content is empty or if it's just an import statement
if (!content || content.trim().startsWith('from "')) {
return null
}
2024-11-17 12:35:56 -05:00
return {
id: `context-${index}`,
2024-11-29 21:49:44 -05:00
type: type as "file" | "code" | "image",
2024-11-17 12:35:56 -05:00
name: name,
content: content,
}
})
2024-11-29 21:49:44 -05:00
.filter(
(tab): tab is NonNullable<typeof tab> =>
tab !== null && tab.content.length > 0
)
}