Files
nexusAI/packages/chat-client/src/components/ProjectModal.jsx
T

178 lines
7.1 KiB
React

import React, { useState, useEffect, useRef } from 'react';
const COLOURS = ['#3d3a79', '#2d6a4f', '#7b2d8b', '#c0392b', '#d4800a', '#1a6b8a'];
export default function ProjectModal({ project, mode, onSave, onDelete, onClose }) {
const [name, setName] = useState(project?.name ?? '');
const [description, setDescription] = useState(project?.description ?? '');
const [colour, setColour] = useState(project?.colour ?? COLOURS[0]);
const [systemPrompt, setSystemPrompt] = useState(project?.system_prompt ?? '');
const inputRef = useRef(null);
useEffect(() => {
if (mode !== 'confirm-delete') inputRef.current?.focus();
}, [mode]);
function handleSubmit() {
const trimmed = name.trim();
if (!trimmed) return;
onSave({
name: trimmed,
description: description.trim() || null,
colour,
icon: null,
isolated: 1,
system_prompt: systemPrompt.trim() || null,
});
onClose();
}
function handleKeyDown(e) {
if (e.key === 'Escape') onClose();
// Don't submit on Enter — textarea fields make Enter ambiguous
}
return (
<div onClick={onClose} style={{
position: 'fixed', inset: 0,
background: 'rgba(0,0,0,0.5)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
zIndex: 100,
}}>
<div onClick={e => e.stopPropagation()} onKeyDown={handleKeyDown} style={{
background: 'var(--bg-surface)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-lg)',
padding: '24px', width: '420px',
maxHeight: '90vh', overflowY: 'auto',
display: 'flex', flexDirection: 'column', gap: '16px',
}}>
{mode === 'confirm-delete' ? (
<>
<h2 style={{ fontSize: '15px', fontWeight: 600, color: 'var(--text-primary)' }}>
Delete project?
</h2>
<p className="text-sm text-secondary">
Are you sure you want to delete{' '}
<span style={{ color: 'var(--text-primary)', fontWeight: 500 }}>{project.name}</span>?
Sessions in this project will not be deleted.
</p>
<div className="flex" style={{ gap: '8px', justifyContent: 'flex-end' }}>
<button className="btn-reset text-base text-muted"
onClick={onClose}
style={{ padding: '8px 14px', borderRadius: 'var(--radius-md)' }}>
Cancel
</button>
<button className="btn-reset text-base"
onClick={() => { onDelete(project.id); onClose(); }}
style={{ padding: '8px 16px', borderRadius: 'var(--radius-md)', background: '#c0392b', color: 'white' }}>
Delete
</button>
</div>
</>
) : (
<>
<h2 style={{ fontSize: '15px', fontWeight: 600, color: 'var(--text-primary)' }}>
{mode === 'create' ? 'New Project' : 'Edit Project'}
</h2>
{/* Name */}
<div className="flex-col" style={{ gap: '6px' }}>
<label className="label-upper">Name</label>
<input
ref={inputRef}
value={name}
onChange={e => setName(e.target.value)}
placeholder="Project name..."
style={{
background: 'var(--bg-elevated)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-md)', padding: '8px 12px',
color: 'var(--text-primary)', fontSize: '14px', outline: 'none', width: '100%',
}}
/>
</div>
{/* Description */}
<div className="flex-col" style={{ gap: '6px' }}>
<label className="label-upper">Description <span style={{ opacity: 0.5 }}>(optional)</span></label>
<textarea
value={description}
onChange={e => setDescription(e.target.value)}
placeholder="What's this project about..."
rows={2}
style={{
background: 'var(--bg-elevated)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-md)', padding: '8px 12px',
color: 'var(--text-primary)', fontSize: '14px', outline: 'none',
width: '100%', resize: 'none', fontFamily: 'inherit',
}}
/>
</div>
{/* Colour picker */}
<div className="flex-col" style={{ gap: '6px' }}>
<label className="label-upper">Colour</label>
<div className="flex" style={{ gap: '8px' }}>
{COLOURS.map(c => (
<button
key={c}
onClick={() => setColour(c)}
className="btn-reset"
style={{
width: '24px', height: '24px',
borderRadius: '50%',
background: c,
border: colour === c ? '2px solid var(--text-primary)' : '2px solid transparent',
outline: colour === c ? '2px solid var(--accent-hover)' : 'none',
outlineOffset: '2px',
}}
/>
))}
</div>
</div>
{/* System Prompt */}
<div className="flex-col" style={{ gap: '6px' }}>
<label className="label-upper">
System Prompt <span style={{ opacity: 0.5 }}>(optional)</span>
</label>
<p className="text-xs text-muted" style={{ marginTop: '-2px' }}>
Overrides the global system prompt for conversations in this project.
Leave blank to use the global default.
</p>
<textarea
value={systemPrompt}
onChange={e => setSystemPrompt(e.target.value)}
placeholder="You are a helpful assistant specialised in..."
rows={4}
style={{
background: 'var(--bg-elevated)', border: '1px solid var(--border)',
borderRadius: 'var(--radius-md)', padding: '8px 12px',
color: 'var(--text-primary)', fontSize: '13px', outline: 'none',
width: '100%', resize: 'vertical', fontFamily: 'inherit',
lineHeight: '1.6',
}}
onFocus={e => e.target.style.borderColor = 'var(--accent)'}
onBlur={e => e.target.style.borderColor = 'var(--border)'}
/>
</div>
<div className="flex" style={{ gap: '8px', justifyContent: 'flex-end' }}>
<button className="btn-reset text-base text-muted"
onClick={onClose}
style={{ padding: '8px 14px', borderRadius: 'var(--radius-md)' }}>
Cancel
</button>
<button className="btn-primary"
onClick={handleSubmit}
disabled={!name.trim()}
style={{ padding: '8px 16px' }}>
{mode === 'create' ? 'Create' : 'Save'}
</button>
</div>
</>
)}
</div>
</div>
);
}