opengraph stuff

This commit is contained in:
2026-02-24 21:47:36 +00:00
parent 05f0d0e8f7
commit e36908cb0b
61 changed files with 5870 additions and 178 deletions
@@ -0,0 +1,23 @@
import Layout from 'emailer/emails/layouts/MainLayout.jsx';
import { Container, Text } from '@react-email/components';
type JobEmailData = {
job: { type: string; completedAt?: number };
};
const Email = ({ job }: JobEmailData) => {
return (
<Layout>
<Container>
<Text className="pt-4 text-2xl">Job Completed</Text>
<Text>
{job?.type === 'gmail-sync'
? 'Your Google account is now completely synced.'
: `Your ${job?.type ?? 'unknown'} job has finished successfully.`}
</Text>
</Container>
</Layout>
);
};
export default Email;
@@ -0,0 +1,20 @@
import Layout from 'emailer/emails/layouts/MainLayout.jsx';
import { Container, Text } from '@react-email/components';
type JobEmailData = {
job: { type: string; error?: string };
};
const Email = ({ job }: JobEmailData) => {
return (
<Layout>
<Container>
<Text className="pt-4 text-2xl">Job Failed</Text>
<Text>Your {job?.type ?? 'unknown'} job has failed.</Text>
{job?.error ? <Text className="text-sm text-gray-600">{job.error}</Text> : null}
</Container>
</Layout>
);
};
export default Email;
+1
View File
@@ -16,6 +16,7 @@ export { useChatWebSocket } from './useChatWebSocket';
export { useDataControl } from './useDataControl';
export { useCustomSorter } from './useCustomSorter';
export { useJobs, useJob } from './useJobs';
export { useAuth } from './useAuth';
export { useForm } from './useForm';
export { useFullscreen } from './useFullscreen';
+72
View File
@@ -0,0 +1,72 @@
import type { Job } from 'types';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useClient } from './useClient';
type JobFilters = {
lane?: string;
type?: string;
status?: string;
};
function buildQuery(filters?: JobFilters) {
const params = new URLSearchParams();
if (filters?.lane) params.set('lane', filters.lane);
if (filters?.type) params.set('type', filters.type);
if (filters?.status) params.set('status', filters.status);
const qs = params.toString();
return qs ? `/queue/jobs?${qs}` : '/queue/jobs';
}
export const useJobs = (filters?: JobFilters) => {
const client = useClient();
const queryClient = useQueryClient();
const { data: jobs = [], refetch } = useQuery<Job[]>({
queryKey: ['jobs', filters],
queryFn: () => client.get<Job[]>(buildQuery(filters)),
refetchInterval: (query) => {
const data = query.state.data;
if (!data) return 3000;
const hasActive = data.some((j) => j.status === 'queued' || j.status === 'running');
return hasActive ? 3000 : false;
},
});
const invalidate = () => queryClient.invalidateQueries({ queryKey: ['jobs'] });
const createMutation = useMutation({
mutationFn: (params: { lane: string; type: string; meta?: Record<string, unknown> }) =>
client.post<Job>('/queue/jobs', params),
onSuccess: invalidate,
});
const cancelMutation = useMutation({
mutationFn: (id: string) => client.delete<Job>(`/queue/jobs/${id}`),
onSuccess: invalidate,
});
return {
jobs,
refetch,
createJob: createMutation.mutateAsync,
cancelJob: cancelMutation.mutateAsync,
};
};
export const useJob = (id: string | null) => {
const client = useClient();
const { data: job } = useQuery<Job>({
queryKey: ['job', id],
queryFn: () => client.get<Job>(`/queue/jobs/${id}`),
enabled: !!id,
refetchInterval: (query) => {
const data = query.state.data;
if (!data) return 2000;
const isActive = data.status === 'queued' || data.status === 'running';
return isActive ? 2000 : false;
},
});
return { job: job ?? null };
};
@@ -40,9 +40,10 @@ type PiMonoInnerProps = {
cwd: { root?: string; path: string };
initialModel: string | null;
taskInfo: TaskInfo;
sandboxed?: boolean;
};
const PiMonoInner = ({ defaultInput, cwd, initialModel, taskInfo }: PiMonoInnerProps) => {
const PiMonoInner = ({ defaultInput, cwd, initialModel, taskInfo, sandboxed }: PiMonoInnerProps) => {
const [phase, setPhase] = useState<Phase>('ready');
const chat = usePiChat(undefined, initialModel, { replaceUrl: false, taskInfo });
const availableModels = useVisiblePiModels();
@@ -108,7 +109,7 @@ const PiMonoInner = ({ defaultInput, cwd, initialModel, taskInfo }: PiMonoInnerP
const handleRun = () => {
setPhase('running');
chat.sendPrompt(defaultInput, undefined, undefined, cwd);
chat.sendPrompt(defaultInput, undefined, undefined, cwd, undefined, sandboxed);
};
if (phase === 'ready') {
@@ -187,9 +188,11 @@ type TaskRunnerModalProps = {
entryType?: 'file' | 'directory';
cwd?: { root?: string; path: string };
promptOverride?: string;
description?: string;
sandboxed?: boolean;
};
export const TaskRunnerModal = ({ open, onOpenChange, task, entryName, entryFullPath, entryType, cwd = { path: '' }, promptOverride }: TaskRunnerModalProps) => {
export const TaskRunnerModal = ({ open, onOpenChange, task, entryName, entryFullPath, entryType, cwd = { path: '' }, promptOverride, description, sandboxed }: TaskRunnerModalProps) => {
const { settings } = useSettings();
const taskSettings = settings.tasks;
const entryRef = entryFullPath ?? entryName;
@@ -209,14 +212,19 @@ export const TaskRunnerModal = ({ open, onOpenChange, task, entryName, entryFull
style={cardStyle()}
>
{/* Header */}
<div className="shrink-0 flex items-center gap-3 px-5 py-3 border-b border-duck-dark/10 bg-background/60">
<div className="flex-1 min-w-0">
<span className="text-sm font-semibold text-duck-dark truncate block">{task.name}</span>
<span className="text-xs text-duck-dark/50 truncate block">{entryName}</span>
<div className="shrink-0 flex flex-col border-b border-duck-dark/10 bg-background/60">
<div className="flex items-center gap-3 px-5 py-3">
<div className="flex-1 min-w-0">
<span className="text-sm font-semibold text-duck-dark truncate block">{task.name}</span>
<span className="text-xs text-duck-dark/50 truncate block">{entryName}</span>
</div>
<DialogPrimitive.Close className="p-1.5 rounded-md text-duck-dark/40 hover:text-duck-dark hover:bg-duck-dark/5 transition-colors cursor-pointer">
<X className="h-4 w-4" />
</DialogPrimitive.Close>
</div>
<DialogPrimitive.Close className="p-1.5 rounded-md text-duck-dark/40 hover:text-duck-dark hover:bg-duck-dark/5 transition-colors cursor-pointer">
<X className="h-4 w-4" />
</DialogPrimitive.Close>
{description && (
<p className="px-5 pb-3 text-xs text-duck-dark/50 dark:text-foreground/50 -mt-1">{description}</p>
)}
</div>
{/* Task Runner */}
@@ -226,6 +234,7 @@ export const TaskRunnerModal = ({ open, onOpenChange, task, entryName, entryFull
cwd={cwd}
initialModel={taskSettings.defaultProvider === 'pi' ? taskSettings.defaultModel : null}
taskInfo={taskInfo}
sandboxed={sandboxed}
/>
</DialogPrimitive.Content>
</DialogPortal>
+16
View File
@@ -0,0 +1,16 @@
export type EmailSummary = {
id: string;
from: string;
to: string;
subject: string;
date: string;
snippet: string;
read?: boolean;
};
export type EmailMessage = EmailSummary & {
cc?: string;
html?: string;
text?: string;
attachments: Array<{ filename: string; size: number; contentType: string }>;
};
+3
View File
@@ -1,6 +1,9 @@
export type {} from './globals';
export * from 'officerdb/types';
export type { Job, JobStep, JobStatus, JobStepStatus, JobProgress } from './queue';
export type { EmailSummary, EmailMessage } from './email';
export type SelectOption = { value: number | string; label?: string; href?: string };
export type StateSetter<T> = React.Dispatch<React.SetStateAction<T>>;
+33
View File
@@ -0,0 +1,33 @@
export type JobStatus = 'queued' | 'running' | 'completed' | 'failed' | 'cancelled';
export type JobStepStatus = 'pending' | 'running' | 'completed' | 'failed';
export type JobProgress = {
current: number;
total: number;
label?: string;
};
export type JobStep = {
name: string;
status: JobStepStatus;
startedAt?: number;
completedAt?: number;
error?: string;
progress?: JobProgress;
};
export type Job = {
id: string;
lane: string;
type: string;
userId: string;
status: JobStatus;
steps: JobStep[];
currentStep: number;
error?: string;
createdAt: number;
startedAt?: number;
completedAt?: number;
meta?: Record<string, unknown>;
};