Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a8b4717
feat(marketing): add DNA extraction, competitor analysis, and strateg…
rafchen Mar 6, 2026
14827e3
Merge remote-tracking branch 'origin/feature/marketing-pipeline'
rafchen Mar 6, 2026
8254aad
fix: marketing pipeline build and type errors
rafchen Mar 6, 2026
0235d9d
UI improvement in the campaign draft to display the output in a preview
rafchen Mar 7, 2026
8ebbb1d
pipeline to rewrite feature w/o having to reroute the user
rafchen Mar 7, 2026
539eaf8
copy to platform is now platform aware for formatting
rafchen Mar 7, 2026
8fe9cb1
larger full document card formanual pipeline rewrite
rafchen Mar 7, 2026
80dc3d7
minor change
mariam-hedgie Mar 8, 2026
706287b
Add structured schemas for evidence citations and normalized company …
mariam-hedgie Mar 9, 2026
7c86554
Implement company knowledge retrieval and normalization pipeline
mariam-hedgie Mar 9, 2026
a87df1c
Add backend intelligence layer and enhance company knowledge retrieva…
mariam-hedgie Mar 9, 2026
ee1bb47
Refactor extractCompanyDNA to utilize validated company knowledge and…
mariam-hedgie Mar 9, 2026
6735b35
feat(marketing-pipeline): persist rewrite workflow state and add rewr…
JunzheShi0702 Mar 9, 2026
9029ea3
Merge branch 'main' into feature/marketing-pipeline
JunzheShi0702 Mar 9, 2026
67970c5
fixed the invalid response body when trying to save the rewrite onto …
rafchen Mar 9, 2026
5fb9e7c
Merge branch 'feature/marketing-pipeline' into junzhe-marketing-pipel…
rafchen Mar 9, 2026
208cb32
Merge pull request #246 from Deodat-Lawson/junzhe-marketing-pipeline-…
rafchen Mar 10, 2026
bb5ff04
refactored code to be accessible with the new ui
rafchen Mar 10, 2026
5373021
display trend reference
rafchen Mar 10, 2026
78d19dc
changes to reduce runtime of marketing pipeline + button ui fix
rafchen Mar 10, 2026
64c17dc
Merge main into feature/marketing-pipeline
rafchen Mar 24, 2026
2109881
fixed sessions ui for dark mode
rafchen Mar 24, 2026
2880ade
marketing pipeline test and improvement
Deodat-Lawson Mar 24, 2026
d807ec3
Merge pull request #254 from Deodat-Lawson/feature/marketing-pipeline
Deodat-Lawson Mar 24, 2026
7a6e84a
Merge branch 'stable' into main
Deodat-Lawson Mar 24, 2026
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
5 changes: 5 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"recommendations": [
"openai.chatgpt"
]
}
48 changes: 41 additions & 7 deletions __tests__/components/RewritePreviewPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,23 @@ import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom";

jest.mock("react-markdown", () => {
return function ReactMarkdown({ children }: { children: string }) {
let processed = children.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
processed = processed.replace(/\*(.*?)\*/g, "<em>$1</em>");
return <div dangerouslySetInnerHTML={{ __html: processed }} />;
};
});

jest.mock("remark-gfm", () => () => {});
jest.mock("remark-math", () => () => {});
jest.mock("rehype-katex", () => () => {});

import { RewritePreviewPanel } from "~/app/employer/documents/components/generator/RewritePreviewPanel";

describe("RewritePreviewPanel", () => {
it("renders before/after diff and Accept/Reject/Try again buttons", () => {
it("renders before/after diff and Push to Rewrite/Reject/Regenerate buttons", () => {
const onAccept = jest.fn();
const onReject = jest.fn();
const onTryAgain = jest.fn();
Expand All @@ -22,12 +35,12 @@ describe("RewritePreviewPanel", () => {
/>
);

expect(screen.getByText("Accept")).toBeInTheDocument();
expect(screen.getByText("Push to Rewrite")).toBeInTheDocument();
expect(screen.getByText("Reject")).toBeInTheDocument();
expect(screen.getByText("Try Again")).toBeInTheDocument();
expect(screen.getByText("Regenerate")).toBeInTheDocument();
});

it("calls onAccept when Accept is clicked", async () => {
it("calls onAccept when Push to Rewrite is clicked", async () => {
const onAccept = jest.fn();
const onReject = jest.fn();
const onTryAgain = jest.fn();
Expand All @@ -42,7 +55,7 @@ describe("RewritePreviewPanel", () => {
/>
);

await userEvent.click(screen.getByText("Accept"));
await userEvent.click(screen.getByText("Push to Rewrite"));
expect(onAccept).toHaveBeenCalledTimes(1);
});

Expand All @@ -65,7 +78,7 @@ describe("RewritePreviewPanel", () => {
expect(onReject).toHaveBeenCalledTimes(1);
});

it("calls onTryAgain when Try Again is clicked", async () => {
it("calls onTryAgain when Regenerate is clicked", async () => {
const onAccept = jest.fn();
const onReject = jest.fn();
const onTryAgain = jest.fn();
Expand All @@ -80,7 +93,28 @@ describe("RewritePreviewPanel", () => {
/>
);

await userEvent.click(screen.getByText("Try Again"));
await userEvent.click(screen.getByText("Regenerate"));
expect(onTryAgain).toHaveBeenCalledTimes(1);
});

it("renders markdown formatting in clean view", async () => {
const onAccept = jest.fn();
const onReject = jest.fn();
const onTryAgain = jest.fn();

render(
<RewritePreviewPanel
originalText="Plain text"
proposedText="**Bold** and *italic*"
onAccept={onAccept}
onReject={onReject}
onTryAgain={onTryAgain}
/>
);

await userEvent.click(screen.getByRole("tab", { name: /clean view/i }));

expect(screen.getByText("Bold", { selector: "strong" })).toBeInTheDocument();
expect(screen.getByText("italic", { selector: "em" })).toBeInTheDocument();
});
});
3 changes: 2 additions & 1 deletion src/app/api/agents/documentQ&A/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export { buildReferences, extractRecommendedPages, filterPagesByAICitation } fro
export { performTavilySearch } from "./tavilySearch";
export { executeWebSearchAgent } from "./webSearchAgent";
export { SYSTEM_PROMPTS, getSystemPrompt, getWebSearchInstruction } from "./prompts";
export { getChatModel, getEmbeddings, getChatModelForProvider, getProviderDefaultModel, describeOllamaError, describeProviderError } from "./models";
export { getChatModel, getEmbeddings } from "./models";
export { getChatModelForProvider, getProviderDefaultModel, describeOllamaError, describeProviderError } from "~/lib/ai/chat-model-factory";
export { ProviderModelMap, ProviderDefaultModels } from "./types";

// RLM Search (hierarchical, cost-aware retrieval for large documents)
Expand Down
113 changes: 7 additions & 106 deletions src/app/api/agents/documentQ&A/services/models.ts
Original file line number Diff line number Diff line change
@@ -1,108 +1,9 @@
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { ChatAnthropic } from "@langchain/anthropic";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
import type { AIModelType } from "./types";
export { getChatModelForProvider, getProviderDefaultModel, describeOllamaError, describeProviderError } from "~/lib/ai/chat-model-factory";

// Re-export type for convenience
export type { AIModelType };

/**
* Get a chat model instance based on the model type
*
* Supports all model types defined in types.ts:
* - OpenAI: gpt-5.2, gpt-5.1, gpt-5-nano, gpt-5-mini
* - Anthropic: claude-sonnet-4, claude-opus-4.5
* - Google: gemini-2.5-flash, gemini-3-flash, gemini-3-pro
* Document Q&A chat model factory.
* Re-exports from shared lib so one place controls model config.
*/
export function getChatModel(modelType: AIModelType): BaseChatModel {
switch (modelType) {
// OpenAI Models
case "gpt-5.2":
return new ChatOpenAI({
openAIApiKey: process.env.OPENAI_API_KEY,
modelName: "gpt-5.2",
temperature: 0.7,
timeout: 600000,
});

case "gpt-5.1":
return new ChatOpenAI({
openAIApiKey: process.env.OPENAI_API_KEY,
modelName: "gpt-5.1",
temperature: 0.7,
timeout: 600000,
});

case "gpt-5-nano":
return new ChatOpenAI({
openAIApiKey: process.env.OPENAI_API_KEY,
modelName: "gpt-5-nano-2025-08-07",
timeout: 300000,
});

case "gpt-5-mini":
return new ChatOpenAI({
openAIApiKey: process.env.OPENAI_API_KEY,
modelName: "gpt-5-mini-2025-08-07",
timeout: 600000,
});

// Anthropic Models
case "claude-sonnet-4":
return new ChatAnthropic({
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
modelName: "claude-sonnet-4-20250514",
temperature: 0.7,
});

case "claude-opus-4.5":
return new ChatAnthropic({
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
modelName: "claude-opus-4.5",
temperature: 0.7,
});

// Google Gemini Models
case "gemini-2.5-flash":
return new ChatGoogleGenerativeAI({
apiKey: process.env.GOOGLE_AI_API_KEY,
model: "gemini-2.5-flash",
temperature: 0.7,
});

case "gemini-3-flash":
return new ChatGoogleGenerativeAI({
apiKey: process.env.GOOGLE_AI_API_KEY,
model: "gemini-3-flash-preview",
temperature: 0.7,
});

case "gemini-3-pro":
return new ChatGoogleGenerativeAI({
apiKey: process.env.GOOGLE_AI_API_KEY,
model: "gemini-3-pro-preview",
temperature: 0.7,
});

// Default fallback
default:
return new ChatOpenAI({
openAIApiKey: process.env.OPENAI_API_KEY,
modelName: "gpt-5-mini",
temperature: 0.7,
timeout: 600000,
});
}
}

/**
* Get embeddings instance
*/
export function getEmbeddings(): OpenAIEmbeddings {
return new OpenAIEmbeddings({
model: "text-embedding-ada-002",
openAIApiKey: process.env.OPENAI_API_KEY,
});
}
export {
getChatModel,
getEmbeddings,
type AIModelType,
} from "~/lib/models";
46 changes: 32 additions & 14 deletions src/app/api/document-generator/generate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Guidelines:
- Apply the requested tone if specified
- Keep similar length to the original unless otherwise specified
- Output ONLY the rewritten text: no quotation marks, no "Here is the rewrite:", no wrapper text
- Use HTML tags for formatting: <strong> for bold, <em> for italic, <u> for underline. Do NOT use Markdown (** or *).`,
- Use Markdown for formatting: **bold**, *italic*, __underline__. Do NOT use raw HTML tags.`,

summarize: `You are an expert editor specializing in summarization. Your task is to create a concise summary of the given text.

Expand Down Expand Up @@ -140,7 +140,15 @@ export async function POST(request: Request) {
);
}

const body = await request.json() as unknown;
let body: unknown;
try {
body = (await request.json()) as unknown;
} catch {
return NextResponse.json(
{ success: false, message: "Invalid JSON body", error: "Request body must be valid JSON" },
{ status: 400 },
);
}
const validation = GenerateSchema.safeParse(body);

if (!validation.success) {
Expand All @@ -153,8 +161,8 @@ export async function POST(request: Request) {
const { action, content, prompt, context, options } = validation.data;
const startTime = Date.now();

// Get the AI model
const modelId = (options?.model ?? "gpt-5-mini") as AIModelType;
// Get the AI model (gpt-4o is widely available; gpt-5-mini may require newer API access)
const modelId = (options?.model ?? "gpt-4o") as AIModelType;
const chat = getChatModel(modelId);

// Build the system prompt
Expand Down Expand Up @@ -221,19 +229,25 @@ export async function POST(request: Request) {
const firstDraft = normalizeModelContent(firstPass.content);

// Refining through second pass
const refinementInstructions = [
"Improve sentence flow and rhythm",
"Remove any redundancy or filler phrases",
"Ensure it reads naturally, not like it was AI-generated",
"Preserve all factual information, names, numbers, and technical terms",
prompt
? "IMPORTANT: The user requested specific additions or changes. Make sure these are fully incorporated and prioritized: " + prompt
: "Do not change the meaning or add new information beyond what was requested",
"Output ONLY the refined text: no quotation marks, no wrapper phrases",
].join("\n- ");

const secondPass = await chat.call([
new SystemMessage(systemPrompt),
new HumanMessage(`Here is a rewritten version of the original text:

"${firstDraft}"

Now refine it further:
- Improve sentence flow and rhythm
- Remove any redundancy or filler phrases
- Ensure it reads naturally, not like it was AI-generated
- Preserve all factual information, names, numbers, and technical terms
- Do not change the meaning or add new information
- Output ONLY the refined text: no quotation marks, no wrapper phrases`),
- ${refinementInstructions}`),
]);

// Use the refined second pass for rewrite (skip the generic call below)
Expand Down Expand Up @@ -287,13 +301,17 @@ Now refine it further:
});

} catch (error) {
const errMessage = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
console.error("[document-generator/generate] error:", error);
return NextResponse.json(
{
success: false,
{
success: false,
message: "Failed to generate content",
error: error instanceof Error ? error.message : "Unknown error"
error: errMessage,
...(process.env.NODE_ENV === "development" && stack ? { stack } : {}),
},
{ status: 500 }
{ status: 500, headers: { "Content-Type": "application/json" } },
);
}
}
Loading
Loading