Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,170 changes: 188 additions & 2,982 deletions components/ChatClient.tsx

Large diffs are not rendered by default.

97 changes: 97 additions & 0 deletions components/chat/ChatHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"use client";

import React from 'react';
import type { PersonaMode } from '../../lib/persona';
import { APP_NAME, STATUS_CONNECTED } from '../../lib/ui-strings';

interface ChatHeaderProps {
personaMode: PersonaMode;
onPersonaModeChange: (mode: PersonaMode) => void;
onUploadMirror: () => void;
onUploadWeather: () => void;
canRecoverStoredPayload: boolean;
onRecoverStoredPayload: () => void;
onStartWrapUp: () => void;
}

export default function ChatHeader({
personaMode,
onPersonaModeChange,
onUploadMirror,
onUploadWeather,
canRecoverStoredPayload,
onRecoverStoredPayload,
onStartWrapUp,
}: ChatHeaderProps) {
return (
<header className="border-b border-slate-800/60 bg-slate-900/70 backdrop-blur-sm">
<div className="mx-auto flex w-full max-w-5xl flex-col gap-4 px-6 py-6 md:flex-row md:items-center md:justify-between">
<div className="space-y-2">
<div className="text-xs uppercase tracking-[0.2em] text-slate-400">
Raven Calder · Poetic Brain
</div>
<h1 className="text-2xl font-semibold text-slate-100">{APP_NAME}</h1>
<p className="text-sm text-slate-400">
Raven is already listening—share what is present, or upload Math Brain and Mirror exports when you are ready for a structured reading.
</p>
<div className="mt-3 flex items-center gap-2 text-xs uppercase tracking-[0.25em] text-emerald-300">
<span className="inline-flex h-2 w-2 rounded-full bg-emerald-400 shadow-[0_0_6px_rgba(16,185,129,0.8)]" />
<span>{STATUS_CONNECTED}</span>
</div>
</div>
<div className="flex flex-wrap gap-2 text-sm">
<div className="inline-flex items-center gap-2 rounded-lg border border-slate-600/60 bg-slate-800/60 px-3 py-2">
<span className="text-[10px] uppercase tracking-[0.2em] text-slate-400">
Persona
</span>
<select
value={personaMode}
onChange={(event) => onPersonaModeChange(event.target.value as PersonaMode)}
className="bg-transparent text-sm font-medium text-slate-100 focus:outline-none"
>
<option value="plain" className="bg-slate-900 text-slate-100">
Plain · Technical
</option>
<option value="hybrid" className="bg-slate-900 text-slate-100">
Hybrid · Default
</option>
<option value="poetic" className="bg-slate-900 text-slate-100">
Poetic · Lyrical
</option>
</select>
</div>
<button
type="button"
onClick={onUploadMirror}
className="rounded-lg border border-slate-600/60 bg-slate-800/60 px-4 py-2 font-medium text-slate-100 hover:border-slate-500 hover:bg-slate-800 transition"
>
🪞 Upload Mirror
</button>
<button
type="button"
onClick={onUploadWeather}
className="rounded-lg border border-slate-600/60 bg-slate-800/60 px-4 py-2 font-medium text-slate-100 hover:border-slate-500 hover:bg-slate-800 transition"
>
🌡️ Upload Weather
</button>
{canRecoverStoredPayload && (
<button
type="button"
onClick={onRecoverStoredPayload}
className="rounded-lg border border-emerald-500/50 bg-emerald-500/10 px-4 py-2 font-medium text-emerald-100 transition hover:bg-emerald-500/20"
>
⏮️ Resume Math Brain
</button>
)}
<button
type="button"
onClick={onStartWrapUp}
className="rounded-lg border border-transparent px-4 py-2 text-slate-400 hover:text-slate-200 transition"
>
Reset Session
</button>
</div>
</div>
</header>
);
}
100 changes: 100 additions & 0 deletions components/chat/ChatInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"use client";

import React, { useRef } from 'react';
import { INPUT_PLACEHOLDER } from '../../lib/ui-strings';

interface ChatInputProps {
input: string;
onInputChange: (value: string) => void;
typing: boolean;
onSend: () => void;
onUploadMirror: () => void;
onUploadWeather: () => void;
onStop: () => void;
fileInputRef: React.RefObject<HTMLInputElement>;
}

export default function ChatInput({
input,
onInputChange,
typing,
onSend,
onUploadMirror,
onUploadWeather,
onStop,
fileInputRef,
}: ChatInputProps) {
const inputRef = useRef<HTMLTextAreaElement | null>(null);

const handleSubmit = (event?: React.FormEvent<HTMLFormElement>) => {
event?.preventDefault();
onSend();
};

return (
<footer className="border-t border-slate-800/70 bg-slate-950/80">
<form
onSubmit={handleSubmit}
className="mx-auto flex w-full max-w-5xl flex-col gap-3 px-6 py-6"
>
<textarea
ref={inputRef}
value={input}
onChange={(event) => onInputChange(event.target.value)}
placeholder={INPUT_PLACEHOLDER}
rows={3}
onKeyDown={(event) => {
if (
event.key === "Enter" &&
!event.shiftKey &&
!event.ctrlKey &&
!event.altKey &&
!event.metaKey
) {
event.preventDefault();
onSend();
}
}}
className="w-full rounded-xl border border-slate-700/60 bg-slate-900/70 px-4 py-3 text-sm text-slate-100 outline-none transition focus:border-slate-400 focus:ring-0"
/>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-wrap gap-2 text-sm">
<button
type="submit"
disabled={!input.trim() || typing}
className="rounded-lg border border-emerald-500/60 bg-emerald-500/20 px-4 py-2 font-medium text-emerald-100 transition hover:bg-emerald-500/30 disabled:cursor-not-allowed disabled:opacity-40"
>
Send
</button>
<button
type="button"
onClick={onUploadMirror}
className="rounded-lg border border-slate-600/60 bg-slate-800/70 px-3 py-2 text-slate-100 transition hover:border-slate-500 hover:bg-slate-800"
>
🪞 Mirror
</button>
<button
type="button"
onClick={onUploadWeather}
className="rounded-lg border border-slate-600/60 bg-slate-800/70 px-3 py-2 text-slate-100 transition hover:border-slate-500 hover:bg-slate-800"
>
🌡️ Weather
</button>
{typing && (
<button
type="button"
onClick={onStop}
className="rounded-lg border border-slate-600/60 bg-slate-900/70 px-3 py-2 text-slate-200 transition hover:bg-slate-800"
>
Stop
</button>
)}
</div>
<div className="text-xs text-slate-500">
Upload Math Brain exports, Mirror JSON, or AstroSeek charts to give Raven geometry.
</div>
</div>
</form>
</footer>
);
}
149 changes: 149 additions & 0 deletions components/chat/MessageList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"use client";

import React from 'react';
import MirrorResponseActions from '../MirrorResponseActions';
import GranularValidation from '../feedback/GranularValidation';
import { hasPendingValidations, getValidationStats, formatValidationSummary } from '@/lib/validation/validationUtils';
import type { ValidationPoint } from '@/lib/validation/types';
import { sanitizeHtml } from '../../lib/chatUtils';
import type { PingResponse } from '../PingFeedback';
import type { Message, ValidationMap } from './types';

interface MessageListProps {
messages: Message[];
typing: boolean;
copiedMessageId: string | null;
validationMap: ValidationMap;
onCopyMessage: (messageId: string, text: string) => void;
onPingFeedback: (messageId:string, response: PingResponse, note?: string) => void;
onValidationUpdate: (messageId: string, points: ValidationPoint[]) => void;
onValidationNoteChange: (messageId: string, pointId: string, note: string) => void;
onStop: () => void;
}

export default function MessageList({
messages,
typing,
copiedMessageId,
validationMap,
onCopyMessage,
onPingFeedback,
onValidationUpdate,
onValidationNoteChange,
onStop,
}: MessageListProps) {
return (
<main className="flex-1 overflow-y-auto">
<div className="mx-auto max-w-3xl space-y-6 px-6 py-10">
{messages.map((msg) => {
const isRaven = msg.role === "raven";
const showCopyButton = isRaven && Boolean(msg.rawText && msg.rawText.trim());
const validationPoints =
validationMap[msg.id] ??
msg.validationPoints ??
[];
const hasValidation = validationPoints.length > 0;
const validationPending = hasValidation && hasPendingValidations(validationPoints);
const validationStats = hasValidation ? getValidationStats(validationPoints) : null;
const validationSummaryText = hasValidation
? validationPending
? `Resonance check in progress: ${validationStats?.completed ?? 0} of ${
validationStats?.total ?? validationPoints.length
} reflections tagged.`
: formatValidationSummary(validationPoints)
: null;

return (
<div
key={msg.id}
className={`flex ${isRaven ? "justify-start" : "justify-end"}`}
>
<div
className={`max-w-full rounded-2xl border px-5 py-4 shadow-lg transition ${
isRaven
? "bg-slate-900/70 border-slate-800/70 text-slate-100"
: "bg-slate-800/80 border-slate-700/60 text-slate-100"
}`}
style={{ width: "100%" }}
>
<div className="mb-3 flex flex-wrap items-center gap-3 text-xs uppercase tracking-[0.2em] text-slate-400">
<span className="font-semibold text-slate-200">
{isRaven ? "Raven" : "You"}
</span>
{msg.climate && <span className="text-slate-400/80">{msg.climate}</span>}
{msg.hook && <span className="text-slate-400/60">{msg.hook}</span>}
</div>
<div className={showCopyButton ? "flex items-start gap-3" : undefined}>
<div
className={`${showCopyButton ? "flex-1" : ""} space-y-3 text-[15px] leading-relaxed text-slate-100`}
dangerouslySetInnerHTML={{ __html: sanitizeHtml(msg.html) }}
/>
{showCopyButton && (
<button
type="button"
onClick={() => onCopyMessage(msg.id, msg.rawText ?? "")}
className="shrink-0 rounded-md border border-slate-700/60 bg-slate-800/70 px-2 py-1 text-[11px] uppercase tracking-[0.2em] text-slate-300 transition hover:border-slate-500 hover:text-slate-100"
>
{copiedMessageId === msg.id ? "Copied" : "Copy"}
</button>
)}
</div>
{isRaven && msg.probe && !msg.pingFeedbackRecorded && (
<div className="mt-4">
<MirrorResponseActions
messageId={msg.id}
onFeedback={onPingFeedback}
checkpointType={"general"}
/>
</div>
)}
{isRaven && hasValidation && (
<div className="mt-4 space-y-3">
{validationSummaryText && (
<p
className={`rounded-md border px-3 py-2 text-xs ${
validationPending
? "border-amber-500/40 bg-amber-500/10 text-amber-200"
: "border-emerald-500/40 bg-emerald-500/10 text-emerald-200"
}`}
>
{validationSummaryText}
</p>
)}
<GranularValidation
messageId={msg.id}
validationPoints={validationPoints}
onComplete={(points) => onValidationUpdate(msg.id, points)}
onNoteChange={(pointId, note) =>
onValidationNoteChange(msg.id, pointId, note)
}
/>
</div>
)}
</div>
</div>
);
})}
{typing && (
<div className="flex justify-start">
<div className="max-w-full rounded-2xl border border-slate-800/70 bg-slate-900/60 px-5 py-4 text-sm text-slate-300 shadow-lg">
<div className="mb-2 text-xs uppercase tracking-[0.2em] text-slate-400">
Raven
</div>
<div className="flex items-center gap-3">
<span className="animate-pulse text-slate-300">Composing…</span>
<button
type="button"
onClick={onStop}
className="rounded-md border border-slate-600/60 px-2 py-1 text-xs text-slate-200 hover:bg-slate-800 transition"
>
Stop
</button>
</div>
</div>
</div>
)}
</div>
</main>
);
}
42 changes: 42 additions & 0 deletions components/chat/RelocationBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use client";

import React from 'react';
import type { RelocationSummary } from '../../lib/relocation';

interface RelocationBannerProps {
relocation: RelocationSummary | null;
}

export default function RelocationBanner({ relocation }: RelocationBannerProps) {
if (!relocation) {
return null;
}

return (
<div className="flex h-full flex-col">
<div className="flex justify-between items-center p-4 border-b border-gray-700 bg-gray-800/50">
<h1 className="text-xl font-semibold">Poetic Brain</h1>
<a
href="/math-brain"
className="text-sm text-blue-400 hover:text-blue-300 flex items-center transition-colors"
title="Return to Math Brain"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back to Math Brain
</a>
</div>
<div className="flex flex-col items-center justify-center p-8 text-center">
<p className="mb-6 max-w-lg text-gray-300">
Welcome to the Poetic Brain. I'm here to help you explore the deeper meanings and patterns in your astrological data.
</p>
{relocation.label && <span className="text-slate-400">• {relocation.label}</span>}
{relocation.status && <span className="text-slate-400">• {relocation.status}</span>}
{relocation.disclosure && (
<span className="text-slate-500">• {relocation.disclosure}</span>
)}
</div>
</div>
);
}
Loading
Loading