Ghostwriter always enabled

This commit is contained in:
DaKheera47 2026-02-15 18:37:38 +00:00
parent 00531c83c4
commit 672eb3d2b9
14 changed files with 17 additions and 148 deletions

View File

@ -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">

View File

@ -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),

View File

@ -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">

View File

@ -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>;

View File

@ -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;

View File

@ -26,7 +26,6 @@ export type SettingKey =
| "jobspyResultsWanted"
| "jobspyCountryIndeed"
| "showSponsorInfo"
| "jobChatEnabled"
| "chatStyleTone"
| "chatStyleFormality"
| "chatStyleConstraints"

View File

@ -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> {

View File

@ -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");

View File

@ -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,

View File

@ -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)],

View File

@ -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,

View File

@ -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(),

View File

@ -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,

View File

@ -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;