Ghostwriter always enabled
This commit is contained in:
parent
00531c83c4
commit
672eb3d2b9
@ -10,7 +10,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import * as api from "../../api";
|
||||
import { useSettings } from "../../hooks/useSettings";
|
||||
import { Composer } from "./Composer";
|
||||
import { MessageList } from "./MessageList";
|
||||
import { RunControls } from "./RunControls";
|
||||
@ -21,9 +20,6 @@ type JobChatPanelProps = {
|
||||
};
|
||||
|
||||
export const JobChatPanel: React.FC<JobChatPanelProps> = ({ job }) => {
|
||||
const { settings } = useSettings();
|
||||
const enabled = settings?.jobChatEnabled ?? false;
|
||||
|
||||
const [threads, setThreads] = useState<JobChatThread[]>([]);
|
||||
const [activeThreadId, setActiveThreadId] = useState<string | null>(null);
|
||||
const [messages, setMessages] = useState<JobChatMessage[]>([]);
|
||||
@ -73,11 +69,6 @@ export const JobChatPanel: React.FC<JobChatPanelProps> = ({ job }) => {
|
||||
}, [job.id, job.title, job.employer]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!enabled) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await api.listJobChatThreads(job.id);
|
||||
@ -96,12 +87,12 @@ export const JobChatPanel: React.FC<JobChatPanelProps> = ({ job }) => {
|
||||
}
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Failed to load job chat";
|
||||
error instanceof Error ? error.message : "Failed to load Ghostwriter";
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [enabled, job.id, createThread, loadThreadMessages]);
|
||||
}, [job.id, createThread, loadThreadMessages]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
@ -295,28 +286,12 @@ export const JobChatPanel: React.FC<JobChatPanelProps> = ({ job }) => {
|
||||
onStreamEvent,
|
||||
]);
|
||||
|
||||
if (!enabled) {
|
||||
return (
|
||||
<Card className="border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
Job Copilot
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Enable this in Settings → Job Chat Copilot.
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="border-border/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
Job Copilot
|
||||
Ghostwriter
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
|
||||
@ -47,7 +47,6 @@ const DEFAULT_FORM_VALUES: UpdateSettingsInput = {
|
||||
resumeProjects: null,
|
||||
rxresumeBaseResumeId: null,
|
||||
showSponsorInfo: null,
|
||||
jobChatEnabled: null,
|
||||
chatStyleTone: "",
|
||||
chatStyleFormality: "",
|
||||
chatStyleConstraints: "",
|
||||
@ -87,7 +86,6 @@ const NULL_SETTINGS_PAYLOAD: UpdateSettingsInput = {
|
||||
resumeProjects: null,
|
||||
rxresumeBaseResumeId: null,
|
||||
showSponsorInfo: null,
|
||||
jobChatEnabled: null,
|
||||
chatStyleTone: null,
|
||||
chatStyleFormality: null,
|
||||
chatStyleConstraints: null,
|
||||
@ -121,7 +119,6 @@ const mapSettingsToForm = (data: AppSettings): UpdateSettingsInput => ({
|
||||
resumeProjects: data.resumeProjects,
|
||||
rxresumeBaseResumeId: data.rxresumeBaseResumeId ?? null,
|
||||
showSponsorInfo: data.overrideShowSponsorInfo,
|
||||
jobChatEnabled: data.overrideJobChatEnabled,
|
||||
chatStyleTone: data.overrideChatStyleTone ?? "",
|
||||
chatStyleFormality: data.overrideChatStyleFormality ?? "",
|
||||
chatStyleConstraints: data.overrideChatStyleConstraints ?? "",
|
||||
@ -217,10 +214,6 @@ const getDerivedSettings = (settings: AppSettings | null) => {
|
||||
default: settings?.defaultShowSponsorInfo ?? true,
|
||||
},
|
||||
chat: {
|
||||
enabled: {
|
||||
effective: settings?.jobChatEnabled ?? false,
|
||||
default: settings?.defaultJobChatEnabled ?? false,
|
||||
},
|
||||
tone: {
|
||||
effective: settings?.chatStyleTone ?? "professional",
|
||||
default: settings?.defaultChatStyleTone ?? "professional",
|
||||
@ -590,7 +583,6 @@ export const SettingsPage: React.FC = () => {
|
||||
resumeProjects: resumeProjectsOverride,
|
||||
rxresumeBaseResumeId: normalizeString(data.rxresumeBaseResumeId),
|
||||
showSponsorInfo: nullIfSame(data.showSponsorInfo, display.default),
|
||||
jobChatEnabled: nullIfSame(data.jobChatEnabled, chat.enabled.default),
|
||||
chatStyleTone: normalizeString(data.chatStyleTone),
|
||||
chatStyleFormality: normalizeString(data.chatStyleFormality),
|
||||
chatStyleConstraints: normalizeString(data.chatStyleConstraints),
|
||||
|
||||
@ -8,7 +8,6 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@ -29,48 +28,20 @@ export const ChatSettingsSection: React.FC<ChatSettingsSectionProps> = ({
|
||||
isLoading,
|
||||
isSaving,
|
||||
}) => {
|
||||
const { enabled, tone, formality, constraints, doNotUse } = values;
|
||||
const { tone, formality, constraints, doNotUse } = values;
|
||||
|
||||
const { control, register, watch } = useFormContext<UpdateSettingsInput>();
|
||||
const isEnabled = watch("jobChatEnabled") ?? enabled.default;
|
||||
const { control, register } = useFormContext<UpdateSettingsInput>();
|
||||
|
||||
return (
|
||||
<AccordionItem value="chat" className="border rounded-lg px-4">
|
||||
<AccordionTrigger className="hover:no-underline py-4">
|
||||
<span className="text-base font-semibold">Job Chat Copilot</span>
|
||||
<span className="text-base font-semibold">Ghostwriter</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<Controller
|
||||
name="jobChatEnabled"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
id="jobChatEnabled"
|
||||
checked={field.value ?? enabled.default}
|
||||
onCheckedChange={(checked) => {
|
||||
field.onChange(
|
||||
checked === "indeterminate" ? null : checked === true,
|
||||
);
|
||||
}}
|
||||
disabled={isLoading || isSaving}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label
|
||||
htmlFor="jobChatEnabled"
|
||||
className="text-sm font-medium leading-none cursor-pointer"
|
||||
>
|
||||
Enable per-job copilot chat
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Adds a persistent chat thread on each job page with prefilled
|
||||
job and profile context.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Ghostwriter is always on. Configure only writing style here.
|
||||
</p>
|
||||
|
||||
<Separator />
|
||||
|
||||
@ -86,7 +57,7 @@ export const ChatSettingsSection: React.FC<ChatSettingsSectionProps> = ({
|
||||
<Select
|
||||
value={field.value ?? tone.default}
|
||||
onValueChange={(value) => field.onChange(value)}
|
||||
disabled={isLoading || isSaving || !isEnabled}
|
||||
disabled={isLoading || isSaving}
|
||||
>
|
||||
<SelectTrigger id="chatStyleTone">
|
||||
<SelectValue placeholder="Select tone" />
|
||||
@ -116,7 +87,7 @@ export const ChatSettingsSection: React.FC<ChatSettingsSectionProps> = ({
|
||||
<Select
|
||||
value={field.value ?? formality.default}
|
||||
onValueChange={(value) => field.onChange(value)}
|
||||
disabled={isLoading || isSaving || !isEnabled}
|
||||
disabled={isLoading || isSaving}
|
||||
>
|
||||
<SelectTrigger id="chatStyleFormality">
|
||||
<SelectValue placeholder="Select formality" />
|
||||
@ -136,8 +107,8 @@ export const ChatSettingsSection: React.FC<ChatSettingsSectionProps> = ({
|
||||
label="Constraints"
|
||||
inputProps={register("chatStyleConstraints")}
|
||||
placeholder="Example: keep answers under 120 words and include bullet points"
|
||||
disabled={isLoading || isSaving || !isEnabled}
|
||||
helper="Optional global writing constraints used by job chat replies."
|
||||
disabled={isLoading || isSaving}
|
||||
helper="Optional global writing constraints used by Ghostwriter replies."
|
||||
current={constraints.effective || "—"}
|
||||
/>
|
||||
|
||||
@ -145,7 +116,7 @@ export const ChatSettingsSection: React.FC<ChatSettingsSectionProps> = ({
|
||||
label="Do-not-use terms"
|
||||
inputProps={register("chatStyleDoNotUse")}
|
||||
placeholder="Example: synergize, leverage"
|
||||
disabled={isLoading || isSaving || !isEnabled}
|
||||
disabled={isLoading || isSaving}
|
||||
helper="Optional comma-separated words or phrases to avoid."
|
||||
current={doNotUse.effective || "—"}
|
||||
/>
|
||||
@ -153,13 +124,6 @@ export const ChatSettingsSection: React.FC<ChatSettingsSectionProps> = ({
|
||||
<Separator />
|
||||
|
||||
<div className="grid gap-2 text-sm sm:grid-cols-2">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">Enabled</div>
|
||||
<div className="break-words font-mono text-xs">
|
||||
Effective: {enabled.effective ? "Yes" : "No"} | Default:{" "}
|
||||
{enabled.default ? "Yes" : "No"}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">Tone</div>
|
||||
<div className="break-words font-mono text-xs">
|
||||
|
||||
@ -15,7 +15,6 @@ export type ModelValues = EffectiveDefault<string> & {
|
||||
export type WebhookValues = EffectiveDefault<string>;
|
||||
export type DisplayValues = EffectiveDefault<boolean>;
|
||||
export type ChatValues = {
|
||||
enabled: EffectiveDefault<boolean>;
|
||||
tone: EffectiveDefault<string>;
|
||||
formality: EffectiveDefault<string>;
|
||||
constraints: EffectiveDefault<string>;
|
||||
|
||||
@ -90,7 +90,7 @@ vi.mock("../../services/job-chat", () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
describe.sequential("Job Chat API", () => {
|
||||
describe.sequential("Ghostwriter API", () => {
|
||||
let server: Server;
|
||||
let baseUrl: string;
|
||||
let closeDb: () => void;
|
||||
|
||||
@ -26,7 +26,6 @@ export type SettingKey =
|
||||
| "jobspyResultsWanted"
|
||||
| "jobspyCountryIndeed"
|
||||
| "showSponsorInfo"
|
||||
| "jobChatEnabled"
|
||||
| "chatStyleTone"
|
||||
| "chatStyleFormality"
|
||||
| "chatStyleConstraints"
|
||||
|
||||
@ -106,7 +106,7 @@ function buildProfileSnapshot(profile: ResumeProfile): string {
|
||||
|
||||
function buildSystemPrompt(style: JobChatStyle): string {
|
||||
return compactJoin([
|
||||
"You are a job application copilot for a single job.",
|
||||
"You are Ghostwriter, a job-application writing assistant for a single job.",
|
||||
"Use only the provided job and profile context unless the user gives extra details.",
|
||||
"Do not claim actions were executed. You are read-only and advisory.",
|
||||
"If details are missing, say what is missing before making assumptions.",
|
||||
@ -145,11 +145,6 @@ async function resolveStyle(): Promise<JobChatStyle> {
|
||||
};
|
||||
}
|
||||
|
||||
export async function isJobChatEnabled(): Promise<boolean> {
|
||||
const overrides = await settingsRepo.getAllSettings();
|
||||
return resolveSettingValue("jobChatEnabled", overrides.jobChatEnabled).value;
|
||||
}
|
||||
|
||||
export async function buildJobChatPromptContext(
|
||||
jobId: string,
|
||||
): Promise<JobChatPromptContext> {
|
||||
|
||||
@ -9,10 +9,7 @@ import {
|
||||
} from "../infra/errors";
|
||||
import * as jobChatRepo from "../repositories/job-chat";
|
||||
import * as settingsRepo from "../repositories/settings";
|
||||
import {
|
||||
buildJobChatPromptContext,
|
||||
isJobChatEnabled,
|
||||
} from "./job-chat-context";
|
||||
import { buildJobChatPromptContext } from "./job-chat-context";
|
||||
import { LlmService } from "./llm/service";
|
||||
import type { JsonSchemaDefinition } from "./llm/types";
|
||||
|
||||
@ -98,15 +95,6 @@ async function buildConversationMessages(
|
||||
}));
|
||||
}
|
||||
|
||||
async function ensureChatEnabled(): Promise<void> {
|
||||
const enabled = await isJobChatEnabled();
|
||||
if (!enabled) {
|
||||
throw badRequest(
|
||||
"Job chat is disabled. Enable it in Settings > Job Chat Copilot.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type GenerateReplyOptions = {
|
||||
jobId: string;
|
||||
threadId: string;
|
||||
@ -145,12 +133,10 @@ export async function createThread(input: {
|
||||
jobId: string;
|
||||
title?: string | null;
|
||||
}) {
|
||||
await ensureChatEnabled();
|
||||
return jobChatRepo.createThread(input);
|
||||
}
|
||||
|
||||
export async function listThreads(jobId: string) {
|
||||
await ensureChatEnabled();
|
||||
return jobChatRepo.listThreadsForJob(jobId);
|
||||
}
|
||||
|
||||
@ -160,7 +146,6 @@ export async function listMessages(input: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}) {
|
||||
await ensureChatEnabled();
|
||||
const thread = await jobChatRepo.getThreadForJob(input.jobId, input.threadId);
|
||||
if (!thread) {
|
||||
throw notFound("Thread not found for this job");
|
||||
@ -175,8 +160,6 @@ export async function listMessages(input: {
|
||||
async function runAssistantReply(
|
||||
options: GenerateReplyOptions,
|
||||
): Promise<{ runId: string; messageId: string; message: string }> {
|
||||
await ensureChatEnabled();
|
||||
|
||||
const thread = await jobChatRepo.getThreadForJob(
|
||||
options.jobId,
|
||||
options.threadId,
|
||||
@ -382,8 +365,6 @@ export async function sendMessage(input: {
|
||||
content: string;
|
||||
stream?: GenerateReplyOptions["stream"];
|
||||
}) {
|
||||
await ensureChatEnabled();
|
||||
|
||||
const content = input.content.trim();
|
||||
if (!content) {
|
||||
throw badRequest("Message content is required");
|
||||
@ -425,8 +406,6 @@ export async function regenerateMessage(input: {
|
||||
assistantMessageId: string;
|
||||
stream?: GenerateReplyOptions["stream"];
|
||||
}) {
|
||||
await ensureChatEnabled();
|
||||
|
||||
const thread = await jobChatRepo.getThreadForJob(input.jobId, input.threadId);
|
||||
if (!thread) {
|
||||
throw notFound("Thread not found for this job");
|
||||
@ -489,8 +468,6 @@ export async function cancelRun(input: {
|
||||
threadId: string;
|
||||
runId: string;
|
||||
}): Promise<{ cancelled: boolean; alreadyFinished: boolean }> {
|
||||
await ensureChatEnabled();
|
||||
|
||||
const run = await jobChatRepo.getRunById(input.runId);
|
||||
if (!run || run.threadId !== input.threadId || run.jobId !== input.jobId) {
|
||||
throw notFound("Run not found for this thread");
|
||||
|
||||
@ -13,7 +13,6 @@ type SettingsConversionValueMap = {
|
||||
jobspyResultsWanted: number;
|
||||
jobspyCountryIndeed: string;
|
||||
showSponsorInfo: boolean;
|
||||
jobChatEnabled: boolean;
|
||||
chatStyleTone: string;
|
||||
chatStyleFormality: string;
|
||||
chatStyleConstraints: string;
|
||||
@ -142,14 +141,6 @@ export const settingsConversionMetadata: SettingsConversionMetadata = {
|
||||
serialize: serializeBitBool,
|
||||
resolve: resolveWithNullishFallback,
|
||||
},
|
||||
jobChatEnabled: {
|
||||
defaultValue: () =>
|
||||
(process.env.JOB_CHAT_ENABLED || "").toLowerCase() === "true" ||
|
||||
process.env.JOB_CHAT_ENABLED === "1",
|
||||
parseOverride: parseBitBoolOrNull,
|
||||
serialize: serializeBitBool,
|
||||
resolve: resolveWithNullishFallback,
|
||||
},
|
||||
chatStyleTone: {
|
||||
defaultValue: () => process.env.CHAT_STYLE_TONE || "professional",
|
||||
parseOverride: (raw) => raw ?? null,
|
||||
|
||||
@ -188,11 +188,6 @@ export const settingsUpdateRegistry: Partial<{
|
||||
actions: [metadataPersistAction("showSponsorInfo", value)],
|
||||
}),
|
||||
),
|
||||
jobChatEnabled: singleAction(({ value }) =>
|
||||
result({
|
||||
actions: [metadataPersistAction("jobChatEnabled", value)],
|
||||
}),
|
||||
),
|
||||
chatStyleTone: singleAction(({ value }) =>
|
||||
result({
|
||||
actions: [metadataPersistAction("chatStyleTone", value)],
|
||||
|
||||
@ -146,14 +146,6 @@ export async function getEffectiveSettings(): Promise<AppSettings> {
|
||||
const overrideShowSponsorInfo = showSponsorInfoSetting.overrideValue;
|
||||
const showSponsorInfo = showSponsorInfoSetting.value;
|
||||
|
||||
const jobChatEnabledSetting = resolveSettingValue(
|
||||
"jobChatEnabled",
|
||||
overrides.jobChatEnabled,
|
||||
);
|
||||
const defaultJobChatEnabled = jobChatEnabledSetting.defaultValue;
|
||||
const overrideJobChatEnabled = jobChatEnabledSetting.overrideValue;
|
||||
const jobChatEnabled = jobChatEnabledSetting.value;
|
||||
|
||||
const chatStyleToneSetting = resolveSettingValue(
|
||||
"chatStyleTone",
|
||||
overrides.chatStyleTone,
|
||||
@ -286,9 +278,6 @@ export async function getEffectiveSettings(): Promise<AppSettings> {
|
||||
showSponsorInfo,
|
||||
defaultShowSponsorInfo,
|
||||
overrideShowSponsorInfo,
|
||||
jobChatEnabled,
|
||||
defaultJobChatEnabled,
|
||||
overrideJobChatEnabled,
|
||||
chatStyleTone,
|
||||
defaultChatStyleTone,
|
||||
overrideChatStyleTone,
|
||||
|
||||
@ -54,7 +54,6 @@ export const updateSettingsSchema = z
|
||||
.optional(),
|
||||
jobspyCountryIndeed: z.string().trim().max(100).nullable().optional(),
|
||||
showSponsorInfo: z.boolean().nullable().optional(),
|
||||
jobChatEnabled: z.boolean().nullable().optional(),
|
||||
chatStyleTone: z.string().trim().max(100).nullable().optional(),
|
||||
chatStyleFormality: z.string().trim().max(100).nullable().optional(),
|
||||
chatStyleConstraints: z.string().trim().max(4000).nullable().optional(),
|
||||
|
||||
@ -179,9 +179,6 @@ export const createAppSettings = (
|
||||
showSponsorInfo: true,
|
||||
defaultShowSponsorInfo: true,
|
||||
overrideShowSponsorInfo: null,
|
||||
jobChatEnabled: false,
|
||||
defaultJobChatEnabled: false,
|
||||
overrideJobChatEnabled: null,
|
||||
chatStyleTone: "professional",
|
||||
defaultChatStyleTone: "professional",
|
||||
overrideChatStyleTone: null,
|
||||
|
||||
@ -913,9 +913,6 @@ export interface AppSettings {
|
||||
showSponsorInfo: boolean;
|
||||
defaultShowSponsorInfo: boolean;
|
||||
overrideShowSponsorInfo: boolean | null;
|
||||
jobChatEnabled: boolean;
|
||||
defaultJobChatEnabled: boolean;
|
||||
overrideJobChatEnabled: boolean | null;
|
||||
chatStyleTone: string;
|
||||
defaultChatStyleTone: string;
|
||||
overrideChatStyleTone: string | null;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user