mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-06 19:35:44 +02:00
commit
57693b36cc
7 changed files with 406 additions and 179 deletions
|
|
@ -4,12 +4,12 @@ import { useRef, useState, createContext, useContext, useCallback, forwardRef, u
|
|||
import { CopilotChatContext } from "../../../lib/types/copilot_types";
|
||||
import { CopilotMessage } from "../../../lib/types/copilot_types";
|
||||
import { CopilotAssistantMessageActionPart } from "../../../lib/types/copilot_types";
|
||||
import { Workflow } from "../../../lib/types/workflow_types";
|
||||
import { Workflow } from "@/app/lib/types/workflow_types";
|
||||
import { z } from "zod";
|
||||
import { getCopilotResponse } from "@/app/actions/copilot_actions";
|
||||
import { Action as WorkflowDispatch } from "../workflow/workflow_editor";
|
||||
import { Panel } from "@/components/common/panel-common";
|
||||
import { ComposeBox } from "@/components/common/compose-box";
|
||||
import { ComposeBoxCopilot } from "@/components/common/compose-box-copilot";
|
||||
import { Messages } from "./components/messages";
|
||||
import { CopyIcon, CheckIcon, PlusIcon, XIcon } from "lucide-react";
|
||||
|
||||
|
|
@ -23,19 +23,21 @@ export function getAppliedChangeKey(messageIndex: number, actionIndex: number, f
|
|||
return `${messageIndex}-${actionIndex}-${field}`;
|
||||
}
|
||||
|
||||
const App = forwardRef(function App({
|
||||
interface AppProps {
|
||||
projectId: string;
|
||||
workflow: z.infer<typeof Workflow>;
|
||||
dispatch: (action: any) => void;
|
||||
chatContext?: any;
|
||||
onCopyJson?: (data: { messages: any[], lastRequest: any, lastResponse: any }) => void;
|
||||
}
|
||||
|
||||
const App = forwardRef<{ handleCopyChat: () => void }, AppProps>(function App({
|
||||
projectId,
|
||||
workflow,
|
||||
dispatch,
|
||||
chatContext = undefined,
|
||||
onCopyJson,
|
||||
}: {
|
||||
projectId: string;
|
||||
workflow: z.infer<typeof Workflow>;
|
||||
dispatch: (action: WorkflowDispatch) => void;
|
||||
chatContext?: z.infer<typeof CopilotChatContext>;
|
||||
onCopyJson: (data: { messages: any[], lastRequest: any, lastResponse: any }) => void;
|
||||
}, ref: Ref<{ handleCopyChat: () => void }>) {
|
||||
}, ref) {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const [messages, setMessages] = useState<z.infer<typeof CopilotMessage>[]>([]);
|
||||
const [loadingResponse, setLoadingResponse] = useState(false);
|
||||
|
|
@ -55,7 +57,7 @@ const App = forwardRef(function App({
|
|||
content: prompt
|
||||
}]);
|
||||
}
|
||||
}, [projectId, messages.length, setMessages]);
|
||||
}, [projectId, messages.length]);
|
||||
|
||||
// Reset discardContext when chatContext changes
|
||||
useEffect(() => {
|
||||
|
|
@ -66,14 +68,11 @@ const App = forwardRef(function App({
|
|||
const effectiveContext = discardContext ? null : chatContext;
|
||||
|
||||
function handleUserMessage(prompt: string) {
|
||||
setMessages([...messages, {
|
||||
setMessages(currentMessages => [...currentMessages, {
|
||||
role: 'user',
|
||||
content: prompt
|
||||
}]);
|
||||
setResponseError(null);
|
||||
// Set loading immediately after adding user message
|
||||
// This ensures ComposeBox clears and disables right away
|
||||
setLoadingResponse(true);
|
||||
}
|
||||
|
||||
const handleApplyChange = useCallback((
|
||||
|
|
@ -176,30 +175,31 @@ const App = forwardRef(function App({
|
|||
}
|
||||
}, [dispatch, appliedChanges, messages]);
|
||||
|
||||
// Second useEffect for copilot response
|
||||
// Effect for handling copilot responses
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
|
||||
async function process() {
|
||||
if (!messages.length) return;
|
||||
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
if (lastMessage.role !== 'user') return;
|
||||
|
||||
setLoadingResponse(true);
|
||||
setResponseError(null);
|
||||
|
||||
try {
|
||||
setLastRequest(null);
|
||||
setLastResponse(null);
|
||||
|
||||
const response = await getCopilotResponse(
|
||||
projectId,
|
||||
messages,
|
||||
workflow,
|
||||
effectiveContext || null,
|
||||
);
|
||||
if (ignore) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ignore) return;
|
||||
|
||||
setLastRequest(response.rawRequest);
|
||||
setLastResponse(response.rawResponse);
|
||||
setMessages([...messages, response.message]);
|
||||
setMessages(currentMessages => [...currentMessages, response.message]);
|
||||
} catch (err) {
|
||||
if (!ignore) {
|
||||
setResponseError(`Failed to get copilot response: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||
|
|
@ -211,43 +211,26 @@ const App = forwardRef(function App({
|
|||
}
|
||||
}
|
||||
|
||||
// if no messages, return
|
||||
if (messages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if last message is not from role user
|
||||
// or tool, return
|
||||
const last = messages[messages.length - 1];
|
||||
if (responseError) {
|
||||
return;
|
||||
}
|
||||
if (last.role !== 'user') {
|
||||
return;
|
||||
}
|
||||
|
||||
process();
|
||||
|
||||
return () => {
|
||||
ignore = true;
|
||||
};
|
||||
}, [
|
||||
messages,
|
||||
projectId,
|
||||
responseError,
|
||||
workflow,
|
||||
effectiveContext,
|
||||
setLoadingResponse,
|
||||
setMessages,
|
||||
setResponseError
|
||||
]);
|
||||
}, [messages, projectId, workflow, effectiveContext]);
|
||||
|
||||
// Scroll to bottom on new messages
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [messages, loadingResponse]);
|
||||
|
||||
const handleCopyChat = useCallback(() => {
|
||||
onCopyJson({
|
||||
messages,
|
||||
lastRequest,
|
||||
lastResponse,
|
||||
});
|
||||
if (onCopyJson) {
|
||||
onCopyJson({
|
||||
messages,
|
||||
lastRequest,
|
||||
lastResponse,
|
||||
});
|
||||
}
|
||||
}, [messages, lastRequest, lastResponse, onCopyJson]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
|
|
@ -295,9 +278,9 @@ const App = forwardRef(function App({
|
|||
</button>
|
||||
</div>
|
||||
</div>}
|
||||
<ComposeBox
|
||||
<ComposeBoxCopilot
|
||||
handleUserMessage={handleUserMessage}
|
||||
messages={messages as any[]}
|
||||
messages={messages}
|
||||
loading={loadingResponse}
|
||||
disabled={loadingResponse}
|
||||
/>
|
||||
|
|
@ -386,3 +369,4 @@ export function Copilot({
|
|||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,8 +71,8 @@ function AssistantMessage({
|
|||
return (
|
||||
<div className="w-full">
|
||||
<div className="px-4 py-2.5 text-sm leading-relaxed text-gray-700 dark:text-gray-200">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-left">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-left flex flex-col gap-4">
|
||||
{content.response.map((part, actionIndex) => {
|
||||
if (part.type === "text") {
|
||||
return <MarkdownContent key={actionIndex} content={part.content} />;
|
||||
|
|
@ -152,17 +152,19 @@ export function Messages({
|
|||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<div className="flex flex-col space-y-4 px-4 pt-4">
|
||||
<div className="flex flex-col space-y-4">
|
||||
{messages.map((message, index) => (
|
||||
<div key={index}>
|
||||
{renderMessage(message, index)}
|
||||
</div>
|
||||
))}
|
||||
{loadingResponse && <AssistantMessageLoading />}
|
||||
</div>
|
||||
<div ref={messagesEndRef} />
|
||||
<div className="flex flex-col [&>*]:mb-4">
|
||||
{messages.map((message, index) => (
|
||||
<div key={index} className="mb-4">
|
||||
{renderMessage(message, index)}
|
||||
</div>
|
||||
))}
|
||||
{loadingResponse && (
|
||||
<div className="animate-pulse">
|
||||
<AssistantMessageLoading />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -526,7 +526,7 @@ function reducer(state: State, action: Action): State {
|
|||
const existingToolIndex = draft.workflow.tools.findIndex(
|
||||
tool => tool.name === newTool.name
|
||||
);
|
||||
|
||||
|
||||
if (existingToolIndex !== -1) {
|
||||
// Replace existing tool
|
||||
draft.workflow.tools[existingToolIndex] = newTool;
|
||||
|
|
@ -803,9 +803,7 @@ export function WorkflowEditor({
|
|||
View versions
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
|
||||
<div className="border-t border-gray-200 dark:border-gray-700" />
|
||||
|
||||
|
||||
<DropdownSection>
|
||||
<DropdownItem
|
||||
key="clone"
|
||||
|
|
@ -814,7 +812,7 @@ export function WorkflowEditor({
|
|||
>
|
||||
Clone this version
|
||||
</DropdownItem>
|
||||
|
||||
|
||||
<DropdownItem
|
||||
key="publish"
|
||||
startContent={<div className="text-indigo-500"><RadioIcon size={16} /></div>}
|
||||
|
|
@ -823,9 +821,7 @@ export function WorkflowEditor({
|
|||
Make version live
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
|
||||
<div className="border-t border-gray-200 dark:border-gray-700" />
|
||||
|
||||
|
||||
<DropdownSection>
|
||||
<DropdownItem
|
||||
key="clipboard"
|
||||
|
|
|
|||
|
|
@ -7,15 +7,27 @@ import { listProjects, createProject, createProjectFromPrompt } from "../../acti
|
|||
import { useRouter } from 'next/navigation';
|
||||
import { tokens } from "@/app/styles/design-tokens";
|
||||
import clsx from 'clsx';
|
||||
import { templates } from "@/app/lib/project_templates";
|
||||
import { templates, starting_copilot_prompts } from "@/app/lib/project_templates";
|
||||
import { SectionHeading } from "@/components/ui/section-heading";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { TemplateCardsList } from "./components/template-cards-list";
|
||||
import { SearchProjects } from "./components/search-projects";
|
||||
import { CustomPromptCard } from "./components/custom-prompt-card";
|
||||
import { Submit } from "./components/submit-button";
|
||||
import { PageHeading } from "@/components/ui/page-heading";
|
||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||
|
||||
const sectionHeaderStyles = clsx(
|
||||
"text-sm font-medium",
|
||||
"text-gray-900 dark:text-gray-100"
|
||||
);
|
||||
const textareaStyles = clsx(
|
||||
"w-full",
|
||||
"rounded-lg p-3",
|
||||
"border border-gray-200 dark:border-gray-700",
|
||||
"bg-white dark:bg-gray-800",
|
||||
"hover:bg-gray-50 dark:hover:bg-gray-750",
|
||||
"focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20",
|
||||
"placeholder:text-gray-400 dark:placeholder:text-gray-500"
|
||||
);
|
||||
|
||||
export default function App() {
|
||||
const [projects, setProjects] = useState<z.infer<typeof Project>[]>([]);
|
||||
|
|
@ -24,15 +36,19 @@ export default function App() {
|
|||
const [selectedCard, setSelectedCard] = useState<'custom' | any>('custom');
|
||||
const [customPrompt, setCustomPrompt] = useState("Create a customer support assistant with one example agent");
|
||||
const [name, setName] = useState("");
|
||||
const [defaultName, setDefaultName] = useState('Untitled 1');
|
||||
const [defaultName, setDefaultName] = useState('Assistant 1');
|
||||
const [isExamplesExpanded, setIsExamplesExpanded] = useState(false);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<string>('blank');
|
||||
const [showCustomPrompt, setShowCustomPrompt] = useState(false);
|
||||
const [promptError, setPromptError] = useState<string | null>(null);
|
||||
const [hasEditedPrompt, setHasEditedPrompt] = useState(false);
|
||||
|
||||
const getNextUntitledNumber = (projects: z.infer<typeof Project>[]) => {
|
||||
const getNextAssistantNumber = (projects: z.infer<typeof Project>[]) => {
|
||||
const untitledProjects = projects
|
||||
.map(p => p.name)
|
||||
.filter(name => name.startsWith('Untitled '))
|
||||
.filter(name => name.startsWith('Assistant '))
|
||||
.map(name => {
|
||||
const num = parseInt(name.replace('Untitled ', ''));
|
||||
const num = parseInt(name.replace('Assistant ', ''));
|
||||
return isNaN(num) ? 0 : num;
|
||||
});
|
||||
|
||||
|
|
@ -54,8 +70,8 @@ export default function App() {
|
|||
|
||||
setProjects(sortedProjects);
|
||||
setIsLoading(false);
|
||||
const nextNumber = getNextUntitledNumber(sortedProjects);
|
||||
const newDefaultName = `Untitled ${nextNumber}`;
|
||||
const nextNumber = getNextAssistantNumber(sortedProjects);
|
||||
const newDefaultName = `Assistant ${nextNumber}`;
|
||||
setDefaultName(newDefaultName);
|
||||
setName(newDefaultName);
|
||||
}
|
||||
|
|
@ -80,44 +96,66 @@ export default function App() {
|
|||
|
||||
const router = useRouter();
|
||||
|
||||
async function handleSubmit(formData: FormData) {
|
||||
// Check if it's a template (from templates object) or a copilot prompt
|
||||
const isTemplate = selectedCard?.id && selectedCard.id in templates;
|
||||
|
||||
if (selectedCard === 'custom' || !isTemplate) {
|
||||
// Handle custom prompt or copilot starting prompts
|
||||
console.log('Creating project from prompt');
|
||||
try {
|
||||
const newFormData = new FormData();
|
||||
newFormData.append('name', name);
|
||||
newFormData.append('prompt', selectedCard === 'custom' ? customPrompt : selectedCard.prompt);
|
||||
|
||||
const response = await createProjectFromPrompt(newFormData);
|
||||
|
||||
if (!response?.id) {
|
||||
throw new Error('Project creation failed');
|
||||
}
|
||||
|
||||
// Store prompt in local storage
|
||||
const promptToStore = selectedCard === 'custom' ? customPrompt : selectedCard.prompt;
|
||||
if (promptToStore) {
|
||||
localStorage.setItem(`project_prompt_${response.id}`, promptToStore);
|
||||
}
|
||||
router.push(`/projects/${response.id}/workflow`);
|
||||
} catch (error) {
|
||||
console.error('Error creating project:', error);
|
||||
}
|
||||
const handleTemplateChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value;
|
||||
setSelectedTemplate(value);
|
||||
|
||||
if (value === 'blank') {
|
||||
setShowCustomPrompt(false);
|
||||
setCustomPrompt('');
|
||||
} else if (value === 'custom') {
|
||||
setShowCustomPrompt(true);
|
||||
setCustomPrompt('');
|
||||
} else {
|
||||
// Handle regular template
|
||||
console.log('Creating template project');
|
||||
try {
|
||||
// Handle example prompts
|
||||
const prompt = starting_copilot_prompts[value];
|
||||
if (prompt) {
|
||||
setShowCustomPrompt(true);
|
||||
setCustomPrompt(prompt);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const validatePrompt = (value: string) => {
|
||||
if (!value.trim()) {
|
||||
return { valid: false, errorMessage: "Prompt cannot be empty" };
|
||||
}
|
||||
return { valid: true };
|
||||
};
|
||||
|
||||
async function handleSubmit(formData: FormData) {
|
||||
try {
|
||||
// Validate prompt if custom prompt section is shown
|
||||
if (showCustomPrompt && !customPrompt.trim()) {
|
||||
setPromptError("Prompt cannot be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
let response;
|
||||
|
||||
if (selectedTemplate === 'blank') {
|
||||
const newFormData = new FormData();
|
||||
newFormData.append('name', name);
|
||||
newFormData.append('template', selectedCard.id);
|
||||
return await createProject(newFormData);
|
||||
} catch (error) {
|
||||
console.error('Error creating project:', error);
|
||||
newFormData.append('template', 'default');
|
||||
response = await createProject(newFormData);
|
||||
} else {
|
||||
const newFormData = new FormData();
|
||||
newFormData.append('name', name);
|
||||
newFormData.append('prompt', customPrompt);
|
||||
response = await createProjectFromPrompt(newFormData);
|
||||
|
||||
if (response?.id && customPrompt) {
|
||||
localStorage.setItem(`project_prompt_${response.id}`, customPrompt);
|
||||
}
|
||||
}
|
||||
|
||||
if (!response?.id) {
|
||||
throw new Error('Project creation failed');
|
||||
}
|
||||
|
||||
router.push(`/projects/${response.id}/workflow`);
|
||||
} catch (error) {
|
||||
console.error('Error creating project:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -159,75 +197,124 @@ export default function App() {
|
|||
{/* Right side: Project Creation */}
|
||||
<div className="overflow-auto">
|
||||
<section className="card h-full">
|
||||
<div className="px-4 pt-4 flex justify-between items-start">
|
||||
<div>
|
||||
<SectionHeading
|
||||
subheading="Set up a new AI assistant"
|
||||
>
|
||||
Create a new project
|
||||
</SectionHeading>
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<Submit />
|
||||
</div>
|
||||
<div className="px-4 pt-4">
|
||||
<SectionHeading subheading="Set up a new AI assistant">
|
||||
Create a new project
|
||||
</SectionHeading>
|
||||
</div>
|
||||
|
||||
<form
|
||||
id="create-project-form"
|
||||
action={handleSubmit}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="px-4 pt-4 pb-8 space-y-6"
|
||||
className="px-4 pt-4 pb-8 space-y-8"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<SectionHeading>Name your assistant</SectionHeading>
|
||||
<Textarea
|
||||
required
|
||||
name="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="min-h-[60px] px-4 py-3"
|
||||
placeholder={defaultName}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="template" value={selectedCard} />
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<SectionHeading>Start with your own prompt</SectionHeading>
|
||||
<CustomPromptCard
|
||||
selected={selectedCard === 'custom'}
|
||||
onSelect={() => handleCardSelect('custom')}
|
||||
customPrompt={customPrompt}
|
||||
onCustomPromptChange={setCustomPrompt}
|
||||
{/* Name Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className={sectionHeaderStyles}>
|
||||
Name
|
||||
</label>
|
||||
<Textarea
|
||||
required
|
||||
name="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className={clsx(
|
||||
textareaStyles,
|
||||
"min-h-[60px]",
|
||||
"text-base",
|
||||
"text-gray-900 dark:text-gray-100"
|
||||
)}
|
||||
placeholder={defaultName}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsExamplesExpanded(!isExamplesExpanded)}
|
||||
className="flex items-center gap-2 w-full"
|
||||
>
|
||||
<div className="flex-1 text-left">
|
||||
<SectionHeading>
|
||||
Or choose an example
|
||||
</SectionHeading>
|
||||
</div>
|
||||
{isExamplesExpanded ? (
|
||||
<ChevronUp className="w-5 h-5 text-gray-500" />
|
||||
) : (
|
||||
<ChevronDown className="w-5 h-5 text-gray-500" />
|
||||
</div>
|
||||
|
||||
{/* Template Selection Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className={sectionHeaderStyles}>
|
||||
Choose how to start
|
||||
</label>
|
||||
<select
|
||||
value={selectedTemplate}
|
||||
onChange={handleTemplateChange}
|
||||
className={clsx(
|
||||
"w-[400px]",
|
||||
"px-4 py-2",
|
||||
"pr-8",
|
||||
"rounded-lg",
|
||||
"border border-gray-200 dark:border-gray-700",
|
||||
"bg-white dark:bg-gray-800",
|
||||
"hover:bg-gray-50 dark:hover:bg-gray-750",
|
||||
"focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20",
|
||||
"appearance-none",
|
||||
"text-base",
|
||||
"text-gray-900 dark:text-gray-100",
|
||||
"bg-[url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22currentColor%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpolyline%20points%3D%226%209%2012%2015%2018%209%22%3E%3C%2Fpolyline%3E%3C%2Fsvg%3E')]",
|
||||
"bg-[length:1.25em]",
|
||||
"bg-[calc(100%-8px)_center]",
|
||||
"bg-no-repeat",
|
||||
"dark:bg-[url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23ffffff%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpolyline%20points%3D%226%209%2012%2015%2018%209%22%3E%3C%2Fpolyline%3E%3C%2Fsvg%3E')]"
|
||||
)}
|
||||
</button>
|
||||
{isExamplesExpanded && (
|
||||
<TemplateCardsList
|
||||
selectedCard={selectedCard}
|
||||
onSelectCard={handleCardSelect}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<option value="blank">Start with a blank template</option>
|
||||
<option value="custom">Write your own starting prompt</option>
|
||||
<optgroup label="Example Prompts">
|
||||
{starting_copilot_prompts &&
|
||||
Object.entries(starting_copilot_prompts)
|
||||
.filter(([name]) => name !== 'Blank Template')
|
||||
.map(([name, prompt]) => (
|
||||
<option key={name} value={name}>
|
||||
{name}
|
||||
</option>
|
||||
))
|
||||
}
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom Prompt Section - Only show when needed */}
|
||||
{showCustomPrompt && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className={sectionHeaderStyles}>
|
||||
{selectedTemplate === 'custom' ? 'Write your prompt' : 'Customize the prompt'}
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
<Textarea
|
||||
value={customPrompt}
|
||||
onChange={(e) => {
|
||||
setCustomPrompt(e.target.value);
|
||||
setPromptError(null);
|
||||
}}
|
||||
placeholder="Example: Create a customer support assistant that can handle product inquiries and returns"
|
||||
className={clsx(
|
||||
textareaStyles,
|
||||
"min-h-[100px]",
|
||||
"text-base",
|
||||
"text-gray-900 dark:text-gray-100",
|
||||
promptError && "border-red-500 focus:ring-red-500/20"
|
||||
)}
|
||||
autoResize
|
||||
required
|
||||
/>
|
||||
{promptError && (
|
||||
<p className="text-sm text-red-500">
|
||||
{promptError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="pt-6 w-full">
|
||||
<Submit />
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,13 +9,15 @@ interface CustomPromptCardProps {
|
|||
onSelect: () => void;
|
||||
customPrompt: string;
|
||||
onCustomPromptChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export function CustomPromptCard({
|
||||
selected,
|
||||
onSelect,
|
||||
customPrompt,
|
||||
onCustomPromptChange
|
||||
onCustomPromptChange,
|
||||
placeholder
|
||||
}: CustomPromptCardProps) {
|
||||
const DEFAULT_PROMPT = "Create a customer support assistant with one example agent";
|
||||
|
||||
|
|
@ -55,7 +57,7 @@ export function CustomPromptCard({
|
|||
tokens.colors.light.text.primary,
|
||||
tokens.colors.dark.text.primary
|
||||
)}>
|
||||
Custom Prompt
|
||||
Prompt
|
||||
</h3>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
|
@ -65,6 +67,7 @@ export function CustomPromptCard({
|
|||
<Textarea
|
||||
value={customPrompt}
|
||||
onChange={(e) => onCustomPromptChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className={clsx(
|
||||
"w-full min-h-[100px]",
|
||||
"resize-none",
|
||||
|
|
|
|||
147
apps/rowboat/components/common/compose-box-copilot.tsx
Normal file
147
apps/rowboat/components/common/compose-box-copilot.tsx
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
'use client';
|
||||
|
||||
import { Button, Spinner } from "@heroui/react";
|
||||
import { useRef, useState, useEffect } from "react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
// Add a type to support both message formats
|
||||
type FlexibleMessage = {
|
||||
role: 'user' | 'assistant' | 'system' | 'tool';
|
||||
content: string | any;
|
||||
version?: string;
|
||||
chatId?: string;
|
||||
createdAt?: string;
|
||||
// Add any other optional fields that might be needed
|
||||
};
|
||||
|
||||
export function ComposeBoxCopilot({
|
||||
minRows=3,
|
||||
disabled=false,
|
||||
loading=false,
|
||||
handleUserMessage,
|
||||
messages,
|
||||
}: {
|
||||
minRows?: number;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
handleUserMessage: (prompt: string) => void;
|
||||
messages: FlexibleMessage[]; // Use the flexible message type
|
||||
}) {
|
||||
const [input, setInput] = useState('');
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
function handleInput() {
|
||||
const prompt = input.trim();
|
||||
if (!prompt) {
|
||||
return;
|
||||
}
|
||||
setInput('');
|
||||
handleUserMessage(prompt);
|
||||
}
|
||||
|
||||
function handleInputKeyDown(event: React.KeyboardEvent<HTMLTextAreaElement>) {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
handleInput();
|
||||
}
|
||||
}
|
||||
|
||||
// focus on the input field only when there is at least one message
|
||||
useEffect(() => {
|
||||
if (messages.length > 0) {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
{/* Keyboard shortcut hint */}
|
||||
<div className="absolute -top-6 right-0 text-xs text-gray-500 dark:text-gray-400 opacity-0
|
||||
group-hover:opacity-100 transition-opacity">
|
||||
Press ⌘ + Enter to send
|
||||
</div>
|
||||
|
||||
{/* Outer container with padding */}
|
||||
<div className="rounded-2xl border-[1.5px] border-gray-200 dark:border-[#2a2d31] p-3 relative
|
||||
bg-white dark:bg-[#1e2023] flex items-end gap-2">
|
||||
{/* Textarea */}
|
||||
<div className="flex-1">
|
||||
<Textarea
|
||||
ref={inputRef}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
disabled={disabled || loading}
|
||||
placeholder="Type a message..."
|
||||
autoResize={true}
|
||||
maxHeight={120}
|
||||
className={`
|
||||
!min-h-0
|
||||
!border-0 !shadow-none !ring-0
|
||||
bg-transparent
|
||||
resize-none
|
||||
overflow-y-auto
|
||||
[&::-webkit-scrollbar]:w-1
|
||||
[&::-webkit-scrollbar-track]:bg-transparent
|
||||
[&::-webkit-scrollbar-thumb]:bg-gray-300
|
||||
[&::-webkit-scrollbar-thumb]:dark:bg-[#2a2d31]
|
||||
[&::-webkit-scrollbar-thumb]:rounded-full
|
||||
placeholder:text-gray-500 dark:placeholder:text-gray-400
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Send button */}
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
disabled={disabled || loading || !input.trim()}
|
||||
onPress={handleInput}
|
||||
className={`
|
||||
transition-all duration-200
|
||||
${input.trim()
|
||||
? 'bg-indigo-50 hover:bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:hover:bg-indigo-800/60 dark:text-indigo-300'
|
||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-500'
|
||||
}
|
||||
scale-100 hover:scale-105 active:scale-95
|
||||
disabled:opacity-50 disabled:scale-95
|
||||
hover:shadow-md dark:hover:shadow-indigo-950/10
|
||||
mb-0.5
|
||||
`}
|
||||
>
|
||||
{loading ? (
|
||||
<Spinner size="sm" color={input.trim() ? "primary" : "default"} />
|
||||
) : (
|
||||
<SendIcon
|
||||
size={16}
|
||||
className={`transform transition-transform ${isFocused ? 'translate-x-0.5' : ''}`}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Custom SendIcon component for better visual alignment
|
||||
function SendIcon({ size, className }: { size: number, className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M22 2L11 13" />
|
||||
<path d="M22 2L15 22L11 13L2 9L22 2Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
@ -32,18 +32,22 @@ export function ComposeBox({
|
|||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
function handleInput() {
|
||||
console.log('handleInput called');
|
||||
const prompt = input.trim();
|
||||
if (!prompt) {
|
||||
console.log('Prompt is empty, returning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear input before calling handleUserMessage
|
||||
console.log('Clearing input');
|
||||
setInput('');
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = '';
|
||||
}
|
||||
|
||||
console.log('Calling handleUserMessage with prompt:', prompt);
|
||||
handleUserMessage(prompt);
|
||||
console.log('handleInput completed');
|
||||
}
|
||||
|
||||
function handleInputKeyDown(event: React.KeyboardEvent<HTMLTextAreaElement>) {
|
||||
|
|
@ -60,6 +64,10 @@ export function ComposeBox({
|
|||
}
|
||||
}, [messages, input]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Input state changed to:', input);
|
||||
}, [input]);
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
{/* Keyboard shortcut hint */}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue