mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-28 09:56:23 +02:00
Mega UI revamp
This commit is contained in:
parent
650f481a96
commit
bcb686a20d
94 changed files with 6984 additions and 3889 deletions
145
apps/rowboat/components/common/compose-box.tsx
Normal file
145
apps/rowboat/components/common/compose-box.tsx
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
'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 ComposeBox({
|
||||
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
|
||||
useEffect(() => {
|
||||
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>
|
||||
);
|
||||
}
|
||||
11
apps/rowboat/components/common/copy-as-json-button.tsx
Normal file
11
apps/rowboat/components/common/copy-as-json-button.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { CopyButton } from "@/components/common/copy-button";
|
||||
|
||||
export function CopyAsJsonButton({ onCopy }: { onCopy: () => void }) {
|
||||
return <div className="absolute top-0 right-0">
|
||||
<CopyButton
|
||||
onCopy={onCopy}
|
||||
label="Copy as JSON"
|
||||
successLabel="Copied"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
41
apps/rowboat/components/common/copy-button.tsx
Normal file
41
apps/rowboat/components/common/copy-button.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
'use client';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CopyIcon, CheckIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
export function CopyButton({
|
||||
onCopy,
|
||||
label,
|
||||
successLabel,
|
||||
}: {
|
||||
onCopy: () => void;
|
||||
label: string;
|
||||
successLabel: string;
|
||||
}) {
|
||||
const [showCopySuccess, setShowCopySuccess] = useState(false);
|
||||
|
||||
const handleCopy = () => {
|
||||
onCopy();
|
||||
setShowCopySuccess(true);
|
||||
setTimeout(() => {
|
||||
setShowCopySuccess(false);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleCopy}
|
||||
className="gap-2"
|
||||
showHoverContent
|
||||
hoverContent={showCopySuccess ? successLabel : label}
|
||||
>
|
||||
{showCopySuccess ? (
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
93
apps/rowboat/components/common/panel-common.tsx
Normal file
93
apps/rowboat/components/common/panel-common.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import clsx from "clsx";
|
||||
|
||||
export function ActionButton({
|
||||
icon = null,
|
||||
children,
|
||||
onClick = undefined,
|
||||
disabled = false,
|
||||
primary = false,
|
||||
}: {
|
||||
icon?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void | undefined;
|
||||
disabled?: boolean;
|
||||
primary?: boolean;
|
||||
}) {
|
||||
const onClickProp = onClick ? { onClick } : {};
|
||||
return <button
|
||||
disabled={disabled}
|
||||
className={clsx("rounded-md text-xs flex items-center gap-1 disabled:text-gray-300 dark:disabled:text-gray-600 hover:text-gray-600 dark:hover:text-gray-300", {
|
||||
"text-blue-600 dark:text-blue-400": primary,
|
||||
"text-gray-400 dark:text-gray-500": !primary,
|
||||
})}
|
||||
{...onClickProp}
|
||||
>
|
||||
{icon}
|
||||
{children}
|
||||
</button>;
|
||||
}
|
||||
|
||||
interface PanelProps {
|
||||
title: React.ReactNode;
|
||||
rightActions?: React.ReactNode;
|
||||
actions?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
maxHeight?: string;
|
||||
variant?: 'default' | 'copilot' | 'projects';
|
||||
}
|
||||
|
||||
export function Panel({
|
||||
title,
|
||||
rightActions,
|
||||
actions,
|
||||
children,
|
||||
maxHeight,
|
||||
variant = 'default',
|
||||
}: PanelProps) {
|
||||
return <div className={clsx(
|
||||
"flex flex-col overflow-hidden rounded-xl border",
|
||||
"border-zinc-200 dark:border-zinc-800",
|
||||
"bg-white dark:bg-zinc-900",
|
||||
maxHeight ? "max-h-[var(--panel-height)]" : "h-full"
|
||||
)}
|
||||
style={{ '--panel-height': maxHeight } as React.CSSProperties}
|
||||
>
|
||||
<div className={clsx(
|
||||
"shrink-0 border-b border-zinc-100 dark:border-zinc-800",
|
||||
variant === 'projects' ? "flex flex-col gap-3 px-4 py-3" : "flex items-center justify-between px-4 py-3"
|
||||
)}>
|
||||
{variant === 'projects' ? (
|
||||
<>
|
||||
<div className="text-sm uppercase tracking-wide text-zinc-500 dark:text-zinc-400">
|
||||
{title}
|
||||
</div>
|
||||
{actions && <div className="flex items-center gap-2">
|
||||
{actions}
|
||||
</div>}
|
||||
</>
|
||||
) : variant === 'copilot' ? (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
{title}
|
||||
</div>
|
||||
{rightActions}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{title}
|
||||
{rightActions}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className={clsx(
|
||||
"min-h-0 flex-1 overflow-y-auto",
|
||||
variant === 'projects' && "custom-scrollbar"
|
||||
)}>
|
||||
{variant === 'projects' ? (
|
||||
<div className="px-3 py-2 pb-4">
|
||||
{children}
|
||||
</div>
|
||||
) : children}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue