autoblogger 0.1.15 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -54
- package/dist/cli/index.js +1224 -0
- package/dist/index.d.mts +54 -8
- package/dist/index.d.ts +54 -8
- package/dist/index.js +932 -99
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +928 -99
- package/dist/index.mjs.map +1 -1
- package/dist/styles/article.d.mts +4 -4
- package/dist/styles/article.d.ts +4 -4
- package/dist/styles/article.js +4 -4
- package/dist/styles/article.js.map +1 -1
- package/dist/styles/article.mjs +4 -4
- package/dist/styles/article.mjs.map +1 -1
- package/dist/styles/autoblogger.css +165 -0
- package/dist/styles/preset.js +48 -8
- package/dist/ui.d.mts +5 -2
- package/dist/ui.d.ts +5 -2
- package/dist/ui.js +1814 -1369
- package/dist/ui.js.map +1 -1
- package/dist/ui.mjs +1734 -1290
- package/dist/ui.mjs.map +1 -1
- package/package.json +15 -2
- package/prisma/schema.prisma +4 -0
package/dist/index.js
CHANGED
|
@@ -31,49 +31,140 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
31
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
32
|
|
|
33
33
|
// src/ai/prompts.ts
|
|
34
|
-
var DEFAULT_GENERATE_TEMPLATE, DEFAULT_CHAT_TEMPLATE, DEFAULT_REWRITE_TEMPLATE, DEFAULT_AUTO_DRAFT_TEMPLATE, DEFAULT_PLAN_TEMPLATE, DEFAULT_PLAN_RULES, DEFAULT_EXPAND_PLAN_TEMPLATE;
|
|
34
|
+
var DEFAULT_GENERATE_TEMPLATE, DEFAULT_CHAT_TEMPLATE, DEFAULT_REWRITE_TEMPLATE, DEFAULT_AUTO_DRAFT_TEMPLATE, DEFAULT_PLAN_TEMPLATE, DEFAULT_PLAN_RULES, DEFAULT_AGENT_TEMPLATE, DEFAULT_EXPAND_PLAN_TEMPLATE;
|
|
35
35
|
var init_prompts = __esm({
|
|
36
36
|
"src/ai/prompts.ts"() {
|
|
37
37
|
"use strict";
|
|
38
|
-
DEFAULT_GENERATE_TEMPLATE =
|
|
38
|
+
DEFAULT_GENERATE_TEMPLATE = `<system>
|
|
39
|
+
<role>Expert essay writer creating engaging, thoughtful content</role>
|
|
39
40
|
|
|
41
|
+
<critical>
|
|
42
|
+
ALWAYS output a complete essay. NEVER respond conversationally.
|
|
43
|
+
- Do NOT ask questions or request clarification
|
|
44
|
+
- Do NOT say "Here is your essay" or similar preamble
|
|
45
|
+
- Do NOT explain what you're going to write
|
|
46
|
+
- If the prompt is vague, make creative choices and proceed
|
|
47
|
+
- Output ONLY the essay in markdown format
|
|
48
|
+
</critical>
|
|
49
|
+
|
|
50
|
+
<rules>
|
|
40
51
|
{{RULES}}
|
|
52
|
+
</rules>
|
|
53
|
+
|
|
54
|
+
<constraints>
|
|
55
|
+
<word_count>{{WORD_COUNT}}</word_count>
|
|
56
|
+
</constraints>
|
|
41
57
|
|
|
42
|
-
|
|
58
|
+
<output_format>
|
|
59
|
+
CRITICAL: Your response MUST start with exactly this format:
|
|
43
60
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
61
|
+
Line 1: # [Your Title Here]
|
|
62
|
+
Line 2: *[Your subtitle here]*
|
|
63
|
+
Line 3: (blank line)
|
|
64
|
+
Line 4+: Essay body in markdown
|
|
47
65
|
|
|
48
|
-
|
|
49
|
-
|
|
66
|
+
<title_guidelines>
|
|
67
|
+
- Be SPECIFIC, not generic (avoid "The Power of", "Why X Matters", "A Guide to")
|
|
68
|
+
- Include a concrete detail, angle, or unexpected element
|
|
69
|
+
- Create curiosity or make a bold claim
|
|
70
|
+
- 5-12 words ideal
|
|
71
|
+
</title_guidelines>
|
|
50
72
|
|
|
73
|
+
<subtitle_guidelines>
|
|
74
|
+
- One sentence that hooks the reader
|
|
75
|
+
- Tease the main argument or reveal a key insight
|
|
76
|
+
- Create tension, curiosity, or promise value
|
|
77
|
+
- Make readers want to continue reading
|
|
78
|
+
</subtitle_guidelines>
|
|
79
|
+
</output_format>
|
|
80
|
+
</system>`;
|
|
81
|
+
DEFAULT_CHAT_TEMPLATE = `<system>
|
|
82
|
+
<role>Helpful writing assistant for essay creation and editing</role>
|
|
83
|
+
|
|
84
|
+
<rules>
|
|
51
85
|
{{CHAT_RULES}}
|
|
86
|
+
</rules>
|
|
87
|
+
|
|
88
|
+
<context>
|
|
89
|
+
{{ESSAY_CONTEXT}}
|
|
90
|
+
</context>
|
|
52
91
|
|
|
53
|
-
|
|
54
|
-
|
|
92
|
+
<behavior>
|
|
93
|
+
- Be concise and actionable
|
|
94
|
+
- When suggesting edits, be specific about what to change
|
|
95
|
+
- Match the author's voice and style when writing
|
|
96
|
+
- Ask clarifying questions if the request is ambiguous
|
|
97
|
+
</behavior>
|
|
98
|
+
</system>`;
|
|
99
|
+
DEFAULT_REWRITE_TEMPLATE = `<system>
|
|
100
|
+
<role>Writing assistant that improves text quality</role>
|
|
55
101
|
|
|
102
|
+
<rules>
|
|
56
103
|
{{REWRITE_RULES}}
|
|
104
|
+
</rules>
|
|
57
105
|
|
|
58
|
-
|
|
59
|
-
|
|
106
|
+
<behavior>
|
|
107
|
+
- Preserve the original meaning exactly
|
|
108
|
+
- Improve clarity, flow, and readability
|
|
109
|
+
- Fix grammar and punctuation issues
|
|
110
|
+
- Maintain the author's voice and tone
|
|
111
|
+
- Output only the improved text, no explanations
|
|
112
|
+
</behavior>
|
|
113
|
+
</system>`;
|
|
114
|
+
DEFAULT_AUTO_DRAFT_TEMPLATE = `<system>
|
|
115
|
+
<role>Expert essay writer creating engaging content from news articles</role>
|
|
60
116
|
|
|
117
|
+
<auto_draft_rules>
|
|
61
118
|
{{AUTO_DRAFT_RULES}}
|
|
119
|
+
</auto_draft_rules>
|
|
62
120
|
|
|
121
|
+
<writing_rules>
|
|
63
122
|
{{RULES}}
|
|
123
|
+
</writing_rules>
|
|
64
124
|
|
|
65
|
-
|
|
66
|
-
|
|
125
|
+
<constraints>
|
|
126
|
+
<word_count>{{AUTO_DRAFT_WORD_COUNT}}</word_count>
|
|
127
|
+
</constraints>
|
|
67
128
|
|
|
68
|
-
|
|
129
|
+
<output_format>
|
|
130
|
+
CRITICAL: Your response MUST start with exactly this format:
|
|
69
131
|
|
|
70
|
-
|
|
132
|
+
Line 1: # [Your Title Here]
|
|
133
|
+
Line 2: *[Your subtitle here]*
|
|
134
|
+
Line 3: (blank line)
|
|
135
|
+
Line 4+: Essay body in markdown
|
|
136
|
+
|
|
137
|
+
<title_guidelines>
|
|
138
|
+
- Be SPECIFIC about the news angle, not generic
|
|
139
|
+
- Include a concrete detail or unexpected element
|
|
140
|
+
- Create curiosity or make a bold claim
|
|
141
|
+
- 5-12 words ideal
|
|
142
|
+
</title_guidelines>
|
|
143
|
+
|
|
144
|
+
<subtitle_guidelines>
|
|
145
|
+
- One sentence that hooks the reader
|
|
146
|
+
- Tease the main argument or unique perspective
|
|
147
|
+
- Create tension, curiosity, or promise value
|
|
148
|
+
</subtitle_guidelines>
|
|
149
|
+
</output_format>
|
|
150
|
+
</system>`;
|
|
151
|
+
DEFAULT_PLAN_TEMPLATE = `<system>
|
|
152
|
+
<role>Writing assistant that creates essay outlines</role>
|
|
71
153
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
154
|
+
<critical>
|
|
155
|
+
Wrap your ENTIRE response in <plan> tags. Output NOTHING outside the tags.
|
|
156
|
+
</critical>
|
|
75
157
|
|
|
76
|
-
|
|
158
|
+
<rules>
|
|
159
|
+
{{PLAN_RULES}}
|
|
160
|
+
</rules>
|
|
161
|
+
|
|
162
|
+
<style_reference>
|
|
163
|
+
{{STYLE_EXAMPLES}}
|
|
164
|
+
</style_reference>
|
|
165
|
+
</system>`;
|
|
166
|
+
DEFAULT_PLAN_RULES = `<format>
|
|
167
|
+
STRICT LIMIT: Maximum 3 bullets per section. Most sections should have 1-2 bullets.
|
|
77
168
|
|
|
78
169
|
<plan>
|
|
79
170
|
# Essay Title
|
|
@@ -89,34 +180,98 @@ Output format:
|
|
|
89
180
|
## Section Name
|
|
90
181
|
- Key point
|
|
91
182
|
</plan>
|
|
183
|
+
</format>
|
|
92
184
|
|
|
93
|
-
|
|
185
|
+
<constraints>
|
|
94
186
|
- 4-6 section headings (## lines)
|
|
95
187
|
- 1-3 bullets per section \u2014 NEVER 4 or more
|
|
96
188
|
- Bullets are short phrases, not sentences
|
|
97
189
|
- No prose, no paragraphs, no explanations
|
|
98
|
-
- When revising, output the complete updated plan
|
|
99
|
-
|
|
190
|
+
- When revising, output the complete updated plan
|
|
191
|
+
</constraints>
|
|
192
|
+
|
|
193
|
+
<title_guidelines>
|
|
194
|
+
- Be SPECIFIC about the essay's angle
|
|
195
|
+
- Include a concrete detail or unexpected element
|
|
196
|
+
- Avoid generic patterns like "The Power of", "Why X Matters"
|
|
197
|
+
- 5-12 words ideal
|
|
198
|
+
</title_guidelines>
|
|
199
|
+
|
|
200
|
+
<subtitle_guidelines>
|
|
201
|
+
- One sentence that previews the main argument
|
|
202
|
+
- Create curiosity or make a bold claim
|
|
203
|
+
</subtitle_guidelines>`;
|
|
204
|
+
DEFAULT_AGENT_TEMPLATE = `<agent_mode>
|
|
205
|
+
You are in AGENT MODE - you can directly edit the essay. Wrap edits in :::edit and ::: tags with a JSON object.
|
|
206
|
+
|
|
207
|
+
EDIT COMMANDS (use valid JSON):
|
|
208
|
+
|
|
209
|
+
1. Replace specific text:
|
|
210
|
+
:::edit
|
|
211
|
+
{"type": "replace_section", "find": "exact text to find", "replace": "replacement text"}
|
|
212
|
+
:::
|
|
100
213
|
|
|
101
|
-
|
|
214
|
+
2. Replace entire essay:
|
|
215
|
+
:::edit
|
|
216
|
+
{"type": "replace_all", "title": "New Title", "subtitle": "New subtitle", "markdown": "Full essay content..."}
|
|
217
|
+
:::
|
|
218
|
+
|
|
219
|
+
3. Insert text:
|
|
220
|
+
:::edit
|
|
221
|
+
{"type": "insert", "position": "after", "find": "text to find", "replace": "text to insert"}
|
|
222
|
+
:::
|
|
223
|
+
(position can be: "before", "after", "start", "end")
|
|
224
|
+
|
|
225
|
+
4. Delete text:
|
|
226
|
+
:::edit
|
|
227
|
+
{"type": "delete", "find": "text to delete"}
|
|
228
|
+
:::
|
|
229
|
+
|
|
230
|
+
RULES:
|
|
231
|
+
- Use EXACT text matches for "find" - copy precisely from the essay
|
|
232
|
+
- One edit block per change
|
|
233
|
+
- You can include multiple edit blocks in one response
|
|
234
|
+
- Add brief explanation before/after edit blocks
|
|
235
|
+
- Edits are applied automatically - the user will see the changes
|
|
236
|
+
</agent_mode>`;
|
|
237
|
+
DEFAULT_EXPAND_PLAN_TEMPLATE = `<system>
|
|
238
|
+
<role>Writing assistant that expands essay outlines into full drafts</role>
|
|
239
|
+
|
|
240
|
+
<writing_rules>
|
|
102
241
|
{{RULES}}
|
|
242
|
+
</writing_rules>
|
|
103
243
|
|
|
104
|
-
|
|
244
|
+
<style_reference>
|
|
105
245
|
{{STYLE_EXAMPLES}}
|
|
246
|
+
</style_reference>
|
|
106
247
|
|
|
107
|
-
|
|
248
|
+
<plan_to_expand>
|
|
249
|
+
{{PLAN}}
|
|
250
|
+
</plan_to_expand>
|
|
108
251
|
|
|
109
|
-
|
|
252
|
+
<output_format>
|
|
253
|
+
CRITICAL: Your response MUST start with exactly this format:
|
|
110
254
|
|
|
111
|
-
|
|
255
|
+
Line 1: # [Title from plan, refined if needed]
|
|
256
|
+
Line 2: *[Subtitle from plan, refined if needed]*
|
|
257
|
+
Line 3: (blank line)
|
|
258
|
+
Line 4+: Essay body with ## section headings
|
|
112
259
|
|
|
113
|
-
|
|
114
|
-
-
|
|
115
|
-
- Follow with the subtitle on line 2 as: *Subtitle here*
|
|
116
|
-
- Use the section headers as H2 headings
|
|
260
|
+
<requirements>
|
|
261
|
+
- Use the section headers from the plan as H2 headings
|
|
117
262
|
- Expand each section's bullet points into full paragraphs
|
|
118
263
|
- Match the author's voice and style from the examples
|
|
119
|
-
- Output ONLY markdown
|
|
264
|
+
- Output ONLY markdown \u2014 no preamble, no "Here is...", no explanations
|
|
265
|
+
</requirements>
|
|
266
|
+
|
|
267
|
+
<title_refinement>
|
|
268
|
+
If the plan title is generic, improve it to be:
|
|
269
|
+
- More specific and concrete
|
|
270
|
+
- Curiosity-inducing or bold
|
|
271
|
+
- 5-12 words
|
|
272
|
+
</title_refinement>
|
|
273
|
+
</output_format>
|
|
274
|
+
</system>`;
|
|
120
275
|
}
|
|
121
276
|
});
|
|
122
277
|
|
|
@@ -165,69 +320,212 @@ var init_models = __esm({
|
|
|
165
320
|
});
|
|
166
321
|
|
|
167
322
|
// src/ai/provider.ts
|
|
323
|
+
var provider_exports = {};
|
|
324
|
+
__export(provider_exports, {
|
|
325
|
+
createStream: () => createStream
|
|
326
|
+
});
|
|
327
|
+
async function fetchSearchResults(query, openaiKey) {
|
|
328
|
+
try {
|
|
329
|
+
console.log("[Web Search] Fetching search results for:", query.slice(0, 100));
|
|
330
|
+
const openai = new import_openai.default({
|
|
331
|
+
...openaiKey && { apiKey: openaiKey }
|
|
332
|
+
});
|
|
333
|
+
const response = await openai.responses.create({
|
|
334
|
+
model: "gpt-5-mini",
|
|
335
|
+
input: `You are a research assistant. Provide a concise summary of the most relevant and recent information from the web about the following query. Include key facts, dates, and sources when available. Keep your response under 500 words.
|
|
336
|
+
|
|
337
|
+
Query: ${query}`,
|
|
338
|
+
tools: [{ type: "web_search" }]
|
|
339
|
+
});
|
|
340
|
+
const result = response.output_text || null;
|
|
341
|
+
console.log("[Web Search] Got results:", result ? `${result.length} chars` : "null");
|
|
342
|
+
return result;
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.error("[Web Search] Failed:", error);
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function extractSearchQuery(messages) {
|
|
349
|
+
const userMessages = messages.filter((m) => m.role === "user");
|
|
350
|
+
return userMessages[userMessages.length - 1]?.content || "";
|
|
351
|
+
}
|
|
168
352
|
async function createStream(options) {
|
|
169
353
|
const modelConfig = getModel(options.model);
|
|
170
354
|
if (!modelConfig) {
|
|
171
355
|
throw new Error(`Unknown model: ${options.model}`);
|
|
172
356
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
357
|
+
let searchContext = "";
|
|
358
|
+
if (options.useWebSearch && modelConfig.provider === "anthropic") {
|
|
359
|
+
const query = extractSearchQuery(options.messages);
|
|
360
|
+
if (query) {
|
|
361
|
+
const searchResults = await fetchSearchResults(query, options.openaiKey);
|
|
362
|
+
if (searchResults) {
|
|
363
|
+
searchContext = `
|
|
364
|
+
|
|
365
|
+
<web_search_results>
|
|
366
|
+
${searchResults}
|
|
367
|
+
</web_search_results>
|
|
368
|
+
|
|
369
|
+
Use the search results above to inform your response with current, accurate information.`;
|
|
370
|
+
}
|
|
176
371
|
}
|
|
177
|
-
|
|
372
|
+
}
|
|
373
|
+
if (modelConfig.provider === "anthropic") {
|
|
374
|
+
return createAnthropicStream(options, modelConfig.modelId, searchContext);
|
|
178
375
|
} else {
|
|
179
|
-
|
|
180
|
-
throw new Error("OpenAI API key not configured");
|
|
181
|
-
}
|
|
182
|
-
return createOpenAIStream(options, modelConfig.modelId);
|
|
376
|
+
return createOpenAIStream(options, modelConfig.modelId, options.useWebSearch);
|
|
183
377
|
}
|
|
184
378
|
}
|
|
185
|
-
async function createAnthropicStream(options, modelId) {
|
|
186
|
-
const anthropic = new import_sdk.default({
|
|
187
|
-
|
|
379
|
+
async function createAnthropicStream(options, modelId, searchContext = "") {
|
|
380
|
+
const anthropic = new import_sdk.default({
|
|
381
|
+
...options.anthropicKey && { apiKey: options.anthropicKey }
|
|
382
|
+
});
|
|
383
|
+
const systemMessage = (options.messages.find((m) => m.role === "system")?.content || "") + searchContext;
|
|
188
384
|
const chatMessages = options.messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: m.content }));
|
|
189
|
-
const
|
|
385
|
+
const requestParams = {
|
|
190
386
|
model: modelId,
|
|
191
387
|
max_tokens: options.maxTokens || 4096,
|
|
192
388
|
system: systemMessage,
|
|
193
389
|
messages: chatMessages
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
390
|
+
};
|
|
391
|
+
if (options.useThinking && (modelId.includes("claude-sonnet") || modelId.includes("claude-opus"))) {
|
|
392
|
+
requestParams.thinking = {
|
|
393
|
+
type: "enabled",
|
|
394
|
+
budget_tokens: 1e4
|
|
395
|
+
};
|
|
396
|
+
requestParams.max_tokens = Math.max(requestParams.max_tokens, 16e3);
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
const stream = await anthropic.messages.stream(requestParams);
|
|
400
|
+
return new ReadableStream({
|
|
401
|
+
async start(controller) {
|
|
402
|
+
try {
|
|
403
|
+
for await (const event of stream) {
|
|
404
|
+
if (event.type === "content_block_delta") {
|
|
405
|
+
const delta = event.delta;
|
|
406
|
+
if (delta.type === "text_delta" && delta.text) {
|
|
407
|
+
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ text: delta.text })}
|
|
408
|
+
|
|
409
|
+
`));
|
|
410
|
+
} else if (delta.type === "thinking_delta" && delta.thinking) {
|
|
411
|
+
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ thinking: delta.thinking })}
|
|
412
|
+
|
|
413
|
+
`));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));
|
|
418
|
+
controller.close();
|
|
419
|
+
} catch (streamError) {
|
|
420
|
+
const errorMessage = streamError instanceof Error ? streamError.message : "Stream error";
|
|
421
|
+
console.error("[Anthropic Stream Error]", streamError);
|
|
422
|
+
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ error: errorMessage })}
|
|
200
423
|
|
|
201
424
|
`));
|
|
425
|
+
controller.close();
|
|
202
426
|
}
|
|
203
427
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
428
|
+
});
|
|
429
|
+
} catch (error) {
|
|
430
|
+
const errorMessage = error instanceof Error ? error.message : "Anthropic API error";
|
|
431
|
+
console.error("[Anthropic API Error]", error);
|
|
432
|
+
throw new Error(errorMessage);
|
|
433
|
+
}
|
|
208
434
|
}
|
|
209
|
-
async function createOpenAIStream(options, modelId) {
|
|
210
|
-
const openai = new import_openai.default({
|
|
211
|
-
|
|
435
|
+
async function createOpenAIStream(options, modelId, useWebSearch = false) {
|
|
436
|
+
const openai = new import_openai.default({
|
|
437
|
+
...options.openaiKey && { apiKey: options.openaiKey }
|
|
438
|
+
});
|
|
439
|
+
if (useWebSearch) {
|
|
440
|
+
return createOpenAIResponsesStream(openai, options, modelId);
|
|
441
|
+
}
|
|
442
|
+
const requestParams = {
|
|
212
443
|
model: modelId,
|
|
213
444
|
messages: options.messages,
|
|
214
|
-
|
|
445
|
+
max_completion_tokens: options.maxTokens || 4096,
|
|
215
446
|
stream: true
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
447
|
+
};
|
|
448
|
+
try {
|
|
449
|
+
const stream = await openai.chat.completions.create(requestParams);
|
|
450
|
+
return new ReadableStream({
|
|
451
|
+
async start(controller) {
|
|
452
|
+
try {
|
|
453
|
+
for await (const chunk of stream) {
|
|
454
|
+
const text = chunk.choices[0]?.delta?.content;
|
|
455
|
+
if (text) {
|
|
456
|
+
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ text })}
|
|
223
457
|
|
|
224
458
|
`));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));
|
|
462
|
+
controller.close();
|
|
463
|
+
} catch (streamError) {
|
|
464
|
+
const errorMessage = streamError instanceof Error ? streamError.message : "Stream error";
|
|
465
|
+
console.error("[OpenAI Stream Error]", streamError);
|
|
466
|
+
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ error: errorMessage })}
|
|
467
|
+
|
|
468
|
+
`));
|
|
469
|
+
controller.close();
|
|
225
470
|
}
|
|
226
471
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
472
|
+
});
|
|
473
|
+
} catch (error) {
|
|
474
|
+
const errorMessage = error instanceof Error ? error.message : "OpenAI API error";
|
|
475
|
+
console.error("[OpenAI API Error]", error);
|
|
476
|
+
throw new Error(errorMessage);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
async function createOpenAIResponsesStream(openai, options, modelId) {
|
|
480
|
+
const systemMessage = options.messages.find((m) => m.role === "system")?.content || "";
|
|
481
|
+
const conversationMessages = options.messages.filter((m) => m.role !== "system");
|
|
482
|
+
const lastUserMessage = conversationMessages[conversationMessages.length - 1]?.content || "";
|
|
483
|
+
const conversationContext = conversationMessages.slice(0, -1).map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`).join("\n\n");
|
|
484
|
+
const fullInput = conversationContext ? `${systemMessage}
|
|
485
|
+
|
|
486
|
+
Previous conversation:
|
|
487
|
+
${conversationContext}
|
|
488
|
+
|
|
489
|
+
User: ${lastUserMessage}` : `${systemMessage}
|
|
490
|
+
|
|
491
|
+
${lastUserMessage}`;
|
|
492
|
+
try {
|
|
493
|
+
const response = await openai.responses.create({
|
|
494
|
+
model: modelId,
|
|
495
|
+
input: fullInput,
|
|
496
|
+
tools: [{ type: "web_search" }],
|
|
497
|
+
stream: true
|
|
498
|
+
});
|
|
499
|
+
return new ReadableStream({
|
|
500
|
+
async start(controller) {
|
|
501
|
+
try {
|
|
502
|
+
for await (const event of response) {
|
|
503
|
+
if (event.type === "response.output_text.delta") {
|
|
504
|
+
const text = event.delta;
|
|
505
|
+
if (text) {
|
|
506
|
+
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ text })}
|
|
507
|
+
|
|
508
|
+
`));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));
|
|
513
|
+
controller.close();
|
|
514
|
+
} catch (streamError) {
|
|
515
|
+
const errorMessage = streamError instanceof Error ? streamError.message : "Stream error";
|
|
516
|
+
console.error("[OpenAI Responses Stream Error]", streamError);
|
|
517
|
+
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ error: errorMessage })}
|
|
518
|
+
|
|
519
|
+
`));
|
|
520
|
+
controller.close();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
} catch (error) {
|
|
525
|
+
const errorMessage = error instanceof Error ? error.message : "OpenAI Responses API error";
|
|
526
|
+
console.error("[OpenAI Responses API Error]", error);
|
|
527
|
+
throw new Error(errorMessage);
|
|
528
|
+
}
|
|
231
529
|
}
|
|
232
530
|
var import_sdk, import_openai;
|
|
233
531
|
var init_provider = __esm({
|
|
@@ -240,9 +538,18 @@ var init_provider = __esm({
|
|
|
240
538
|
});
|
|
241
539
|
|
|
242
540
|
// src/ai/builders.ts
|
|
541
|
+
var builders_exports = {};
|
|
542
|
+
__export(builders_exports, {
|
|
543
|
+
buildAutoDraftPrompt: () => buildAutoDraftPrompt,
|
|
544
|
+
buildChatPrompt: () => buildChatPrompt,
|
|
545
|
+
buildExpandPlanPrompt: () => buildExpandPlanPrompt,
|
|
546
|
+
buildGeneratePrompt: () => buildGeneratePrompt,
|
|
547
|
+
buildPlanPrompt: () => buildPlanPrompt,
|
|
548
|
+
buildRewritePrompt: () => buildRewritePrompt
|
|
549
|
+
});
|
|
243
550
|
function buildGeneratePrompt(options) {
|
|
244
551
|
const template = options.template || DEFAULT_GENERATE_TEMPLATE;
|
|
245
|
-
return template.replace("{{RULES}}", options.rules || "").replace("{{WORD_COUNT}}", String(options.wordCount || 800));
|
|
552
|
+
return template.replace("{{RULES}}", options.rules || "").replace("{{WORD_COUNT}}", String(options.wordCount || 800)).replace("{{STYLE_EXAMPLES}}", options.styleExamples || "");
|
|
246
553
|
}
|
|
247
554
|
function buildChatPrompt(options) {
|
|
248
555
|
const template = options.template || DEFAULT_CHAT_TEMPLATE;
|
|
@@ -257,12 +564,25 @@ Content:
|
|
|
257
564
|
${options.essayContext.markdown}
|
|
258
565
|
`;
|
|
259
566
|
}
|
|
260
|
-
return template.replace("{{CHAT_RULES}}", options.chatRules || "").replace("{{ESSAY_CONTEXT}}", essaySection);
|
|
567
|
+
return template.replace("{{CHAT_RULES}}", options.chatRules || "").replace("{{RULES}}", options.rules || "").replace("{{ESSAY_CONTEXT}}", essaySection).replace("{{STYLE_EXAMPLES}}", options.styleExamples || "");
|
|
261
568
|
}
|
|
262
569
|
function buildExpandPlanPrompt(options) {
|
|
263
570
|
const template = options.template || DEFAULT_EXPAND_PLAN_TEMPLATE;
|
|
264
571
|
return template.replace("{{RULES}}", options.rules || "").replace("{{STYLE_EXAMPLES}}", options.styleExamples || "").replace("{{PLAN}}", options.plan);
|
|
265
572
|
}
|
|
573
|
+
function buildPlanPrompt(options) {
|
|
574
|
+
const template = options.template || DEFAULT_PLAN_TEMPLATE;
|
|
575
|
+
const rules = options.planRules || DEFAULT_PLAN_RULES;
|
|
576
|
+
return template.replace("{{PLAN_RULES}}", rules).replace("{{STYLE_EXAMPLES}}", options.styleExamples || "");
|
|
577
|
+
}
|
|
578
|
+
function buildRewritePrompt(options) {
|
|
579
|
+
const template = options.template || DEFAULT_REWRITE_TEMPLATE;
|
|
580
|
+
return template.replace("{{REWRITE_RULES}}", options.rewriteRules || "").replace("{{RULES}}", options.rules || "").replace("{{STYLE_EXAMPLES}}", options.styleExamples || "");
|
|
581
|
+
}
|
|
582
|
+
function buildAutoDraftPrompt(options) {
|
|
583
|
+
const template = options.template || DEFAULT_AUTO_DRAFT_TEMPLATE;
|
|
584
|
+
return template.replace("{{AUTO_DRAFT_RULES}}", options.autoDraftRules || "").replace("{{RULES}}", options.rules || "").replace("{{AUTO_DRAFT_WORD_COUNT}}", String(options.wordCount || 800)).replace("{{STYLE_EXAMPLES}}", options.styleExamples || "").replace("{{TOPIC_NAME}}", options.topicName || "").replace("{{ARTICLE_TITLE}}", options.articleTitle || "").replace("{{ARTICLE_SUMMARY}}", options.articleSummary || "").replace("{{ARTICLE_URL}}", options.articleUrl || "");
|
|
585
|
+
}
|
|
266
586
|
var init_builders = __esm({
|
|
267
587
|
"src/ai/builders.ts"() {
|
|
268
588
|
"use strict";
|
|
@@ -270,6 +590,228 @@ var init_builders = __esm({
|
|
|
270
590
|
}
|
|
271
591
|
});
|
|
272
592
|
|
|
593
|
+
// src/lib/url-extractor.ts
|
|
594
|
+
var url_extractor_exports = {};
|
|
595
|
+
__export(url_extractor_exports, {
|
|
596
|
+
buildUrlContext: () => buildUrlContext,
|
|
597
|
+
extractAndFetchUrls: () => extractAndFetchUrls,
|
|
598
|
+
extractUrls: () => extractUrls,
|
|
599
|
+
fetchUrlContent: () => fetchUrlContent
|
|
600
|
+
});
|
|
601
|
+
function isServerlessEnvironment() {
|
|
602
|
+
return !!(process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.VERCEL || process.env.NETLIFY || process.env.AWS_EXECUTION_ENV);
|
|
603
|
+
}
|
|
604
|
+
function extractUrls(text) {
|
|
605
|
+
const urls = [];
|
|
606
|
+
const withProtocol = text.match(URL_WITH_PROTOCOL);
|
|
607
|
+
if (withProtocol) urls.push(...withProtocol);
|
|
608
|
+
const wwwUrls = text.match(URL_WITHOUT_PROTOCOL);
|
|
609
|
+
if (wwwUrls) {
|
|
610
|
+
for (const url of wwwUrls) {
|
|
611
|
+
const normalized = `https://${url}`;
|
|
612
|
+
if (!urls.some((u) => u.includes(url))) {
|
|
613
|
+
urls.push(normalized);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const bareUrls = text.match(DOMAIN_ONLY);
|
|
618
|
+
if (bareUrls) {
|
|
619
|
+
for (const url of bareUrls) {
|
|
620
|
+
const normalized = `https://${url}`;
|
|
621
|
+
if (!urls.some((u) => u.includes(url.split("/")[0]))) {
|
|
622
|
+
urls.push(normalized);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return [...new Set(urls)];
|
|
627
|
+
}
|
|
628
|
+
function extractTextFromHtml(html, url) {
|
|
629
|
+
const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
630
|
+
const title = titleMatch ? titleMatch[1].trim() : void 0;
|
|
631
|
+
let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<nav[^>]*>[\s\S]*?<\/nav>/gi, "").replace(/<footer[^>]*>[\s\S]*?<\/footer>/gi, "").replace(/<header[^>]*>[\s\S]*?<\/header>/gi, "").replace(/<aside[^>]*>[\s\S]*?<\/aside>/gi, "").replace(/<!--[\s\S]*?-->/g, "").replace(/<(p|div|br|h[1-6]|li|tr)[^>]*>/gi, "\n").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&[a-z]+;/gi, " ").replace(/\s+/g, " ").replace(/\n\s+/g, "\n").replace(/\n+/g, "\n").trim();
|
|
632
|
+
if (text.length > 4e3) {
|
|
633
|
+
text = text.slice(0, 4e3) + "\n\n[Content truncated...]";
|
|
634
|
+
}
|
|
635
|
+
if (text.length < 50) {
|
|
636
|
+
return { url, content: "", error: "Could not extract meaningful content" };
|
|
637
|
+
}
|
|
638
|
+
return { url, title, content: text };
|
|
639
|
+
}
|
|
640
|
+
async function parseWithReadability(html, url) {
|
|
641
|
+
try {
|
|
642
|
+
const { JSDOM } = await import("jsdom");
|
|
643
|
+
const { Readability } = await import("@mozilla/readability");
|
|
644
|
+
const doc = new JSDOM(html, {
|
|
645
|
+
url,
|
|
646
|
+
resources: void 0,
|
|
647
|
+
// Don't load ANY external resources (stylesheets, etc.)
|
|
648
|
+
runScripts: void 0
|
|
649
|
+
// Don't run any scripts
|
|
650
|
+
});
|
|
651
|
+
const reader = new Readability(doc.window.document);
|
|
652
|
+
const article = reader.parse();
|
|
653
|
+
if (!article || !article.textContent) {
|
|
654
|
+
console.log("[Readability] No article content, falling back to simple extraction");
|
|
655
|
+
return extractTextFromHtml(html, url);
|
|
656
|
+
}
|
|
657
|
+
let content = article.textContent.trim();
|
|
658
|
+
if (content.length > 4e3) {
|
|
659
|
+
content = content.slice(0, 4e3) + "\n\n[Content truncated...]";
|
|
660
|
+
}
|
|
661
|
+
return {
|
|
662
|
+
url,
|
|
663
|
+
title: article.title || void 0,
|
|
664
|
+
content
|
|
665
|
+
};
|
|
666
|
+
} catch (error) {
|
|
667
|
+
console.error("[JSDOM] Failed, using simple extraction:", error instanceof Error ? error.message : error);
|
|
668
|
+
return extractTextFromHtml(html, url);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async function fetchWithPuppeteer(url) {
|
|
672
|
+
let browser = null;
|
|
673
|
+
const isServerless = isServerlessEnvironment();
|
|
674
|
+
try {
|
|
675
|
+
console.log(`[Puppeteer] Launching browser for: ${url} (serverless: ${isServerless})`);
|
|
676
|
+
if (isServerless) {
|
|
677
|
+
const chromium = await import("@sparticuz/chromium");
|
|
678
|
+
const puppeteerCore = await import("puppeteer-core");
|
|
679
|
+
const executablePath = await chromium.default.executablePath();
|
|
680
|
+
browser = await puppeteerCore.default.launch({
|
|
681
|
+
args: chromium.default.args,
|
|
682
|
+
defaultViewport: chromium.default.defaultViewport,
|
|
683
|
+
executablePath,
|
|
684
|
+
headless: chromium.default.headless
|
|
685
|
+
});
|
|
686
|
+
} else {
|
|
687
|
+
try {
|
|
688
|
+
const puppeteer = await import("puppeteer");
|
|
689
|
+
browser = await puppeteer.default.launch({
|
|
690
|
+
headless: true,
|
|
691
|
+
args: [
|
|
692
|
+
"--no-sandbox",
|
|
693
|
+
"--disable-setuid-sandbox",
|
|
694
|
+
"--disable-dev-shm-usage",
|
|
695
|
+
"--disable-accelerated-2d-canvas",
|
|
696
|
+
"--disable-gpu"
|
|
697
|
+
]
|
|
698
|
+
});
|
|
699
|
+
} catch (puppeteerImportError) {
|
|
700
|
+
console.error("[Puppeteer] Import/launch failed:", puppeteerImportError);
|
|
701
|
+
return { url, content: "", error: "Puppeteer unavailable - falling back to simple fetch" };
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
const page = await browser.newPage();
|
|
705
|
+
await page.setViewport({ width: 1920, height: 1080 });
|
|
706
|
+
await page.setUserAgent(
|
|
707
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
708
|
+
);
|
|
709
|
+
await page.setRequestInterception(true);
|
|
710
|
+
page.on("request", (req) => {
|
|
711
|
+
const resourceType = req.resourceType();
|
|
712
|
+
if (["image", "stylesheet", "font", "media"].includes(resourceType)) {
|
|
713
|
+
req.abort();
|
|
714
|
+
} else {
|
|
715
|
+
req.continue();
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
await page.goto(url, {
|
|
719
|
+
waitUntil: "networkidle2",
|
|
720
|
+
timeout: PUPPETEER_TIMEOUT
|
|
721
|
+
});
|
|
722
|
+
await new Promise((resolve) => setTimeout(resolve, CONTENT_WAIT_TIME));
|
|
723
|
+
const html = await page.content();
|
|
724
|
+
await browser.close();
|
|
725
|
+
browser = null;
|
|
726
|
+
console.log("[Puppeteer] Got HTML, parsing with Readability...");
|
|
727
|
+
return await parseWithReadability(html, url);
|
|
728
|
+
} catch (error) {
|
|
729
|
+
const errorMessage = error instanceof Error ? error.message : "Puppeteer error";
|
|
730
|
+
console.error("[Puppeteer] Failed:", errorMessage);
|
|
731
|
+
return { url, content: "", error: `Puppeteer: ${errorMessage}` };
|
|
732
|
+
} finally {
|
|
733
|
+
if (browser) {
|
|
734
|
+
try {
|
|
735
|
+
await browser.close();
|
|
736
|
+
} catch {
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
async function fetchWithSimpleRequest(url) {
|
|
742
|
+
try {
|
|
743
|
+
console.log("[SimpleFetch] Fetching:", url);
|
|
744
|
+
const res = await fetch(url, {
|
|
745
|
+
headers: {
|
|
746
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
747
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
748
|
+
"Accept-Language": "en-US,en;q=0.9"
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
if (!res.ok) {
|
|
752
|
+
return { url, content: "", error: `HTTP ${res.status}` };
|
|
753
|
+
}
|
|
754
|
+
const html = await res.text();
|
|
755
|
+
return parseWithReadability(html, url);
|
|
756
|
+
} catch (error) {
|
|
757
|
+
return {
|
|
758
|
+
url,
|
|
759
|
+
content: "",
|
|
760
|
+
error: error instanceof Error ? error.message : "Failed to fetch"
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
async function fetchUrlContent(url) {
|
|
765
|
+
console.log("[URL Extractor] Fetching content from:", url);
|
|
766
|
+
const puppeteerResult = await fetchWithPuppeteer(url);
|
|
767
|
+
if (!puppeteerResult.error && puppeteerResult.content && puppeteerResult.content.length > 100) {
|
|
768
|
+
console.log("[URL Extractor] Puppeteer succeeded, got", puppeteerResult.content.length, "chars");
|
|
769
|
+
return puppeteerResult;
|
|
770
|
+
}
|
|
771
|
+
console.log("[URL Extractor] Puppeteer failed or got minimal content, trying simple fetch...");
|
|
772
|
+
const simpleResult = await fetchWithSimpleRequest(url);
|
|
773
|
+
if (simpleResult.content && simpleResult.content.length > (puppeteerResult.content?.length || 0)) {
|
|
774
|
+
console.log("[URL Extractor] Simple fetch got more content:", simpleResult.content.length, "chars");
|
|
775
|
+
return simpleResult;
|
|
776
|
+
}
|
|
777
|
+
if (puppeteerResult.content && puppeteerResult.content.length > 0) {
|
|
778
|
+
return puppeteerResult;
|
|
779
|
+
}
|
|
780
|
+
return simpleResult.error ? simpleResult : puppeteerResult;
|
|
781
|
+
}
|
|
782
|
+
async function extractAndFetchUrls(text) {
|
|
783
|
+
const urls = extractUrls(text);
|
|
784
|
+
if (urls.length === 0) return [];
|
|
785
|
+
const toFetch = urls.slice(0, 3);
|
|
786
|
+
const results = await Promise.all(toFetch.map((url) => fetchUrlContent(url)));
|
|
787
|
+
return results;
|
|
788
|
+
}
|
|
789
|
+
function buildUrlContext(fetched) {
|
|
790
|
+
const successful = fetched.filter((f) => !f.error && f.content);
|
|
791
|
+
if (successful.length === 0) return "";
|
|
792
|
+
return `
|
|
793
|
+
<referenced_urls>
|
|
794
|
+
${successful.map(
|
|
795
|
+
(f) => `<url src="${f.url}"${f.title ? ` title="${f.title}"` : ""}>
|
|
796
|
+
${f.content}
|
|
797
|
+
</url>`
|
|
798
|
+
).join("\n\n")}
|
|
799
|
+
</referenced_urls>
|
|
800
|
+
|
|
801
|
+
Use the content from these URLs when relevant to the conversation.`;
|
|
802
|
+
}
|
|
803
|
+
var URL_WITH_PROTOCOL, URL_WITHOUT_PROTOCOL, DOMAIN_ONLY, PUPPETEER_TIMEOUT, CONTENT_WAIT_TIME;
|
|
804
|
+
var init_url_extractor = __esm({
|
|
805
|
+
"src/lib/url-extractor.ts"() {
|
|
806
|
+
"use strict";
|
|
807
|
+
URL_WITH_PROTOCOL = /https?:\/\/[^\s<>\[\]()]+(?:\([^\s<>\[\]()]*\))?[^\s<>\[\]().,;:!?"']*(?<![.,;:!?"'])/gi;
|
|
808
|
+
URL_WITHOUT_PROTOCOL = /(?:www\.)[a-zA-Z0-9][-a-zA-Z0-9]*(?:\.[a-zA-Z]{2,})+(?:\/[^\s<>\[\]()]*)?/gi;
|
|
809
|
+
DOMAIN_ONLY = /(?<![/@])(?:[a-zA-Z0-9][-a-zA-Z0-9]*\.)+(?:com|org|net|edu|gov|io|co|app|dev|news|info)(?:\/[^\s<>\[\]()]*)?(?![a-zA-Z])/gi;
|
|
810
|
+
PUPPETEER_TIMEOUT = 15e3;
|
|
811
|
+
CONTENT_WAIT_TIME = 2e3;
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
|
|
273
815
|
// src/ai/generate.ts
|
|
274
816
|
var generate_exports = {};
|
|
275
817
|
__export(generate_exports, {
|
|
@@ -280,17 +822,41 @@ async function generateStream(options) {
|
|
|
280
822
|
const systemPrompt = buildGeneratePrompt({
|
|
281
823
|
rules: options.rules,
|
|
282
824
|
template: options.template,
|
|
283
|
-
wordCount: options.wordCount
|
|
825
|
+
wordCount: options.wordCount,
|
|
826
|
+
styleExamples: options.styleExamples
|
|
284
827
|
});
|
|
828
|
+
let enrichedPrompt = options.prompt;
|
|
829
|
+
if (options.useWebSearch) {
|
|
830
|
+
try {
|
|
831
|
+
const fetched = await extractAndFetchUrls(options.prompt);
|
|
832
|
+
const successful = fetched.filter((f) => !f.error && f.content);
|
|
833
|
+
if (successful.length > 0) {
|
|
834
|
+
enrichedPrompt = `${options.prompt}
|
|
835
|
+
|
|
836
|
+
<source_material>
|
|
837
|
+
${successful.map(
|
|
838
|
+
(f) => `Source: ${f.url}${f.title ? ` (${f.title})` : ""}
|
|
839
|
+
${f.content}`
|
|
840
|
+
).join("\n\n---\n\n")}
|
|
841
|
+
</source_material>
|
|
842
|
+
|
|
843
|
+
Use the source material above as reference for the essay.`;
|
|
844
|
+
}
|
|
845
|
+
} catch (err) {
|
|
846
|
+
console.warn("URL extraction failed:", err);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
285
849
|
return createStream({
|
|
286
850
|
model: options.model,
|
|
287
851
|
messages: [
|
|
288
852
|
{ role: "system", content: systemPrompt },
|
|
289
|
-
{ role: "user", content:
|
|
853
|
+
{ role: "user", content: enrichedPrompt }
|
|
290
854
|
],
|
|
291
855
|
anthropicKey: options.anthropicKey,
|
|
292
856
|
openaiKey: options.openaiKey,
|
|
293
|
-
maxTokens: 8192
|
|
857
|
+
maxTokens: options.useThinking ? 16e3 : 8192,
|
|
858
|
+
useWebSearch: options.useWebSearch,
|
|
859
|
+
useThinking: options.useThinking
|
|
294
860
|
});
|
|
295
861
|
}
|
|
296
862
|
async function expandPlanStream(options) {
|
|
@@ -316,6 +882,7 @@ var init_generate = __esm({
|
|
|
316
882
|
"use strict";
|
|
317
883
|
init_provider();
|
|
318
884
|
init_builders();
|
|
885
|
+
init_url_extractor();
|
|
319
886
|
}
|
|
320
887
|
});
|
|
321
888
|
|
|
@@ -325,30 +892,107 @@ __export(chat_exports, {
|
|
|
325
892
|
chatStream: () => chatStream
|
|
326
893
|
});
|
|
327
894
|
async function chatStream(options) {
|
|
328
|
-
const systemPrompt =
|
|
895
|
+
const systemPrompt = options.mode === "plan" ? buildPlanPrompt({
|
|
896
|
+
planRules: options.planRules,
|
|
897
|
+
template: options.planTemplate,
|
|
898
|
+
styleExamples: options.styleExamples
|
|
899
|
+
}) : buildChatPrompt({
|
|
329
900
|
chatRules: options.chatRules,
|
|
901
|
+
rules: options.rules,
|
|
330
902
|
template: options.template,
|
|
331
|
-
essayContext: options.essayContext
|
|
903
|
+
essayContext: options.essayContext,
|
|
904
|
+
styleExamples: options.styleExamples
|
|
332
905
|
});
|
|
906
|
+
let urlContext = "";
|
|
907
|
+
let urlExtractionStatus = "";
|
|
908
|
+
if (options.useWebSearch) {
|
|
909
|
+
const lastUserMsg = [...options.messages].reverse().find((m) => m.role === "user");
|
|
910
|
+
if (lastUserMsg) {
|
|
911
|
+
try {
|
|
912
|
+
const { extractUrls: extractUrls2 } = await Promise.resolve().then(() => (init_url_extractor(), url_extractor_exports));
|
|
913
|
+
const detectedUrls = extractUrls2(lastUserMsg.content);
|
|
914
|
+
if (detectedUrls.length > 0) {
|
|
915
|
+
console.log("[URL Extraction] Detected URLs:", detectedUrls);
|
|
916
|
+
const fetched = await extractAndFetchUrls(lastUserMsg.content);
|
|
917
|
+
if (fetched.length > 0) {
|
|
918
|
+
const successful = fetched.filter((f) => !f.error && f.content);
|
|
919
|
+
const failed = fetched.filter((f) => f.error || !f.content);
|
|
920
|
+
if (successful.length > 0) {
|
|
921
|
+
urlContext = buildUrlContext(fetched);
|
|
922
|
+
console.log("[URL Extraction] Successfully fetched:", successful.map((f) => f.url));
|
|
923
|
+
}
|
|
924
|
+
if (failed.length > 0) {
|
|
925
|
+
console.warn("[URL Extraction] Failed to fetch:", failed.map((f) => ({ url: f.url, error: f.error })));
|
|
926
|
+
urlExtractionStatus = `
|
|
927
|
+
|
|
928
|
+
<url_extraction_status>
|
|
929
|
+
Attempted to fetch ${detectedUrls.length} URL(s). ${successful.length} succeeded, ${failed.length} failed.
|
|
930
|
+
${failed.map((f) => `- ${f.url}: ${f.error || "Empty content"}`).join("\n")}
|
|
931
|
+
</url_extraction_status>`;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
} catch (err) {
|
|
936
|
+
console.error("[URL Extraction] Error:", err);
|
|
937
|
+
urlExtractionStatus = `
|
|
938
|
+
|
|
939
|
+
<url_extraction_status>
|
|
940
|
+
URL extraction encountered an error: ${err instanceof Error ? err.message : "Unknown error"}
|
|
941
|
+
</url_extraction_status>`;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
333
945
|
let modeInstructions = "";
|
|
334
946
|
if (options.mode === "agent") {
|
|
335
|
-
modeInstructions =
|
|
336
|
-
You can directly edit the essay using these commands:
|
|
337
|
-
- To replace text: :::edit replace_section "old text" "new text" :::
|
|
338
|
-
- To replace all: :::edit replace_all "title" "subtitle" "full markdown" :::
|
|
339
|
-
- To insert: :::edit insert "position" "match_text" "new_text" ::: (position: before, after, start, end)
|
|
340
|
-
- To delete: :::edit delete "text to remove" :::
|
|
341
|
-
`;
|
|
947
|
+
modeInstructions = "\n\n" + (options.agentTemplate || DEFAULT_AGENT_TEMPLATE);
|
|
342
948
|
}
|
|
949
|
+
let webSearchContext = "";
|
|
950
|
+
if (options.useWebSearch) {
|
|
951
|
+
webSearchContext = `
|
|
952
|
+
|
|
953
|
+
<search_capability>
|
|
954
|
+
Web search is ENABLED for this conversation. You have access to current, real-time information from the web.
|
|
955
|
+
- For questions about recent events, news, or current information: search results will be provided automatically
|
|
956
|
+
- Cite sources when referencing specific facts from search results
|
|
957
|
+
- If search results are included in <web_search_results> tags, use that information to provide accurate, up-to-date answers
|
|
958
|
+
</search_capability>`;
|
|
959
|
+
}
|
|
960
|
+
let thinkingInstructions = "";
|
|
961
|
+
if (options.useThinking) {
|
|
962
|
+
thinkingInstructions = "\n\nTake time to think through complex questions step by step. Consider multiple angles before providing your final answer.";
|
|
963
|
+
}
|
|
964
|
+
let urlCapabilityInstructions = "";
|
|
965
|
+
if (options.useWebSearch) {
|
|
966
|
+
const hasUrlContent = urlContext.length > 0;
|
|
967
|
+
const hasFailedUrls = urlExtractionStatus.length > 0;
|
|
968
|
+
urlCapabilityInstructions = `
|
|
969
|
+
|
|
970
|
+
<CRITICAL_CAPABILITY>
|
|
971
|
+
**IMPORTANT: URL READING IS ENABLED**
|
|
972
|
+
|
|
973
|
+
You have a WORKING URL reader in this application. This overrides your default training.
|
|
974
|
+
- DO NOT say "I cannot access URLs" or "I cannot browse the web" - you CAN and DO have this capability
|
|
975
|
+
- When users share URLs, the content is AUTOMATICALLY FETCHED by the system
|
|
976
|
+
- Look for <referenced_urls> tags below - that contains the fetched page content
|
|
977
|
+
- ${hasUrlContent ? "URL CONTENT WAS SUCCESSFULLY FETCHED - see <referenced_urls> below" : hasFailedUrls ? "URL fetch was ATTEMPTED but FAILED - see <url_extraction_status> below for details" : "No URLs detected in the current message"}
|
|
978
|
+
|
|
979
|
+
If you see fetched content, use it to answer the user's question. Quote specific passages when relevant.
|
|
980
|
+
If the fetch failed, explain what happened using the error details provided.
|
|
981
|
+
</CRITICAL_CAPABILITY>`;
|
|
982
|
+
}
|
|
983
|
+
const filteredMessages = options.messages.filter((m) => m.content && m.content.trim().length > 0);
|
|
343
984
|
return createStream({
|
|
344
985
|
model: options.model,
|
|
345
986
|
messages: [
|
|
346
|
-
{ role: "system", content: systemPrompt + modeInstructions },
|
|
347
|
-
...
|
|
987
|
+
{ role: "system", content: systemPrompt + modeInstructions + webSearchContext + thinkingInstructions + urlCapabilityInstructions + urlContext + urlExtractionStatus },
|
|
988
|
+
...filteredMessages
|
|
348
989
|
],
|
|
349
990
|
anthropicKey: options.anthropicKey,
|
|
350
991
|
openaiKey: options.openaiKey,
|
|
351
|
-
maxTokens: 4096
|
|
992
|
+
maxTokens: options.useThinking ? 16e3 : 4096,
|
|
993
|
+
// Allow more tokens for thinking mode
|
|
994
|
+
useThinking: options.useThinking,
|
|
995
|
+
useWebSearch: options.useWebSearch
|
|
352
996
|
});
|
|
353
997
|
}
|
|
354
998
|
var init_chat = __esm({
|
|
@@ -356,6 +1000,8 @@ var init_chat = __esm({
|
|
|
356
1000
|
"use strict";
|
|
357
1001
|
init_provider();
|
|
358
1002
|
init_builders();
|
|
1003
|
+
init_url_extractor();
|
|
1004
|
+
init_prompts();
|
|
359
1005
|
}
|
|
360
1006
|
});
|
|
361
1007
|
|
|
@@ -373,8 +1019,12 @@ __export(src_exports, {
|
|
|
373
1019
|
DEFAULT_REWRITE_TEMPLATE: () => DEFAULT_REWRITE_TEMPLATE,
|
|
374
1020
|
addCommentMark: () => addCommentMark,
|
|
375
1021
|
applyCommentMarks: () => applyCommentMarks,
|
|
1022
|
+
buildAutoDraftPrompt: () => buildAutoDraftPrompt,
|
|
376
1023
|
buildChatPrompt: () => buildChatPrompt,
|
|
1024
|
+
buildExpandPlanPrompt: () => buildExpandPlanPrompt,
|
|
377
1025
|
buildGeneratePrompt: () => buildGeneratePrompt,
|
|
1026
|
+
buildPlanPrompt: () => buildPlanPrompt,
|
|
1027
|
+
buildRewritePrompt: () => buildRewritePrompt,
|
|
378
1028
|
canDeleteComment: () => canDeleteComment,
|
|
379
1029
|
canEditComment: () => canEditComment,
|
|
380
1030
|
createAPIHandler: () => createAPIHandler,
|
|
@@ -1331,15 +1981,21 @@ async function handleAIAPI(req, cms, session, path) {
|
|
|
1331
1981
|
if (authError) return authError;
|
|
1332
1982
|
if (method === "GET" && path === "/ai/settings") {
|
|
1333
1983
|
const settings = await cms.aiSettings.get();
|
|
1984
|
+
const hasAnthropicEnvKey = !!(cms.config.ai?.anthropicKey || process.env.ANTHROPIC_API_KEY);
|
|
1985
|
+
const hasOpenaiEnvKey = !!(cms.config.ai?.openaiKey || process.env.OPENAI_API_KEY);
|
|
1334
1986
|
return jsonResponse2({
|
|
1335
1987
|
data: {
|
|
1336
1988
|
...settings,
|
|
1989
|
+
// Don't expose actual env keys, just indicate they exist
|
|
1990
|
+
hasAnthropicEnvKey,
|
|
1991
|
+
hasOpenaiEnvKey,
|
|
1337
1992
|
defaultGenerateTemplate: DEFAULT_GENERATE_TEMPLATE,
|
|
1338
1993
|
defaultChatTemplate: DEFAULT_CHAT_TEMPLATE,
|
|
1339
1994
|
defaultRewriteTemplate: DEFAULT_REWRITE_TEMPLATE,
|
|
1340
1995
|
defaultAutoDraftTemplate: DEFAULT_AUTO_DRAFT_TEMPLATE,
|
|
1341
1996
|
defaultPlanTemplate: DEFAULT_PLAN_TEMPLATE,
|
|
1342
1997
|
defaultExpandPlanTemplate: DEFAULT_EXPAND_PLAN_TEMPLATE,
|
|
1998
|
+
defaultAgentTemplate: DEFAULT_AGENT_TEMPLATE,
|
|
1343
1999
|
defaultPlanRules: DEFAULT_PLAN_RULES
|
|
1344
2000
|
}
|
|
1345
2001
|
});
|
|
@@ -1353,11 +2009,36 @@ async function handleAIAPI(req, cms, session, path) {
|
|
|
1353
2009
|
}
|
|
1354
2010
|
if (method === "POST" && path === "/ai/generate") {
|
|
1355
2011
|
const body = await req.json();
|
|
1356
|
-
const { prompt, model, wordCount, mode, plan, styleExamples } = body;
|
|
2012
|
+
const { prompt, model, wordCount, mode, plan, styleExamples: clientStyleExamples, useWebSearch, useThinking } = body;
|
|
1357
2013
|
const settings = await cms.aiSettings.get();
|
|
1358
2014
|
try {
|
|
1359
2015
|
let stream;
|
|
2016
|
+
const anthropicKey = cms.config.ai?.anthropicKey || settings.anthropicKey;
|
|
2017
|
+
const openaiKey = cms.config.ai?.openaiKey || settings.openaiKey;
|
|
1360
2018
|
if (mode === "expand_plan" && plan) {
|
|
2019
|
+
let styleExamples = clientStyleExamples || "";
|
|
2020
|
+
if (!styleExamples) {
|
|
2021
|
+
try {
|
|
2022
|
+
const publishedPosts = await cms.posts.findPublished();
|
|
2023
|
+
const MAX_STYLE_EXAMPLES = 5;
|
|
2024
|
+
const MAX_WORDS_PER_EXAMPLE = 500;
|
|
2025
|
+
if (publishedPosts.length > 0) {
|
|
2026
|
+
const examples = publishedPosts.slice(0, MAX_STYLE_EXAMPLES).map((post) => {
|
|
2027
|
+
const words = post.markdown.split(/\s+/);
|
|
2028
|
+
const truncatedContent = words.length > MAX_WORDS_PER_EXAMPLE ? words.slice(0, MAX_WORDS_PER_EXAMPLE).join(" ") + "..." : post.markdown;
|
|
2029
|
+
return `## ${post.title}
|
|
2030
|
+
${post.subtitle ? `*${post.subtitle}*
|
|
2031
|
+
` : ""}
|
|
2032
|
+
${truncatedContent}`;
|
|
2033
|
+
}).join("\n\n---\n\n");
|
|
2034
|
+
styleExamples = `The following are examples of the author's published work. Use these to match their voice, tone, and writing style:
|
|
2035
|
+
|
|
2036
|
+
${examples}`;
|
|
2037
|
+
}
|
|
2038
|
+
} catch (err) {
|
|
2039
|
+
console.error("[AI Generate] Failed to fetch published essays:", err);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
1361
2042
|
const { expandPlanStream: expandPlanStream2 } = await Promise.resolve().then(() => (init_generate(), generate_exports));
|
|
1362
2043
|
stream = await expandPlanStream2({
|
|
1363
2044
|
plan,
|
|
@@ -1365,10 +2046,33 @@ async function handleAIAPI(req, cms, session, path) {
|
|
|
1365
2046
|
rules: settings.rules,
|
|
1366
2047
|
template: settings.expandPlanTemplate,
|
|
1367
2048
|
styleExamples,
|
|
1368
|
-
anthropicKey
|
|
1369
|
-
openaiKey
|
|
2049
|
+
anthropicKey,
|
|
2050
|
+
openaiKey
|
|
1370
2051
|
});
|
|
1371
2052
|
} else {
|
|
2053
|
+
let styleExamples = clientStyleExamples || "";
|
|
2054
|
+
if (!styleExamples) {
|
|
2055
|
+
try {
|
|
2056
|
+
const publishedPosts = await cms.posts.findPublished();
|
|
2057
|
+
const MAX_STYLE_EXAMPLES = 5;
|
|
2058
|
+
const MAX_WORDS_PER_EXAMPLE = 500;
|
|
2059
|
+
if (publishedPosts.length > 0) {
|
|
2060
|
+
const examples = publishedPosts.slice(0, MAX_STYLE_EXAMPLES).map((post) => {
|
|
2061
|
+
const words = post.markdown.split(/\s+/);
|
|
2062
|
+
const truncatedContent = words.length > MAX_WORDS_PER_EXAMPLE ? words.slice(0, MAX_WORDS_PER_EXAMPLE).join(" ") + "..." : post.markdown;
|
|
2063
|
+
return `## ${post.title}
|
|
2064
|
+
${post.subtitle ? `*${post.subtitle}*
|
|
2065
|
+
` : ""}
|
|
2066
|
+
${truncatedContent}`;
|
|
2067
|
+
}).join("\n\n---\n\n");
|
|
2068
|
+
styleExamples = `The following are examples of the author's published work. Use these to match their voice, tone, and writing style:
|
|
2069
|
+
|
|
2070
|
+
${examples}`;
|
|
2071
|
+
}
|
|
2072
|
+
} catch (err) {
|
|
2073
|
+
console.error("[AI Generate] Failed to fetch published essays:", err);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
1372
2076
|
const { generateStream: generateStream2 } = await Promise.resolve().then(() => (init_generate(), generate_exports));
|
|
1373
2077
|
stream = await generateStream2({
|
|
1374
2078
|
prompt,
|
|
@@ -1376,8 +2080,11 @@ async function handleAIAPI(req, cms, session, path) {
|
|
|
1376
2080
|
wordCount,
|
|
1377
2081
|
rules: settings.rules,
|
|
1378
2082
|
template: settings.generateTemplate,
|
|
1379
|
-
|
|
1380
|
-
|
|
2083
|
+
styleExamples,
|
|
2084
|
+
anthropicKey,
|
|
2085
|
+
openaiKey,
|
|
2086
|
+
useWebSearch,
|
|
2087
|
+
useThinking
|
|
1381
2088
|
});
|
|
1382
2089
|
}
|
|
1383
2090
|
return new Response(stream, {
|
|
@@ -1388,6 +2095,7 @@ async function handleAIAPI(req, cms, session, path) {
|
|
|
1388
2095
|
}
|
|
1389
2096
|
});
|
|
1390
2097
|
} catch (error) {
|
|
2098
|
+
console.error("[AI Generate Error]", error);
|
|
1391
2099
|
return jsonResponse2({
|
|
1392
2100
|
error: error instanceof Error ? error.message : "Generation failed"
|
|
1393
2101
|
}, 500);
|
|
@@ -1395,8 +2103,33 @@ async function handleAIAPI(req, cms, session, path) {
|
|
|
1395
2103
|
}
|
|
1396
2104
|
if (method === "POST" && path === "/ai/chat") {
|
|
1397
2105
|
const body = await req.json();
|
|
1398
|
-
const { messages, model, essayContext, mode } = body;
|
|
2106
|
+
const { messages, model, essayContext, mode, useWebSearch, useThinking } = body;
|
|
1399
2107
|
const settings = await cms.aiSettings.get();
|
|
2108
|
+
const anthropicKey = cms.config.ai?.anthropicKey || settings.anthropicKey;
|
|
2109
|
+
const openaiKey = cms.config.ai?.openaiKey || settings.openaiKey;
|
|
2110
|
+
let styleExamples = "";
|
|
2111
|
+
try {
|
|
2112
|
+
const publishedPosts = await cms.posts.findPublished();
|
|
2113
|
+
const MAX_STYLE_EXAMPLES = 5;
|
|
2114
|
+
const MAX_WORDS_PER_EXAMPLE = 500;
|
|
2115
|
+
if (publishedPosts.length > 0) {
|
|
2116
|
+
const examples = publishedPosts.slice(0, MAX_STYLE_EXAMPLES).map((post) => {
|
|
2117
|
+
const words = post.markdown.split(/\s+/);
|
|
2118
|
+
const truncatedContent = words.length > MAX_WORDS_PER_EXAMPLE ? words.slice(0, MAX_WORDS_PER_EXAMPLE).join(" ") + "..." : post.markdown;
|
|
2119
|
+
return `## ${post.title}
|
|
2120
|
+
${post.subtitle ? `*${post.subtitle}*
|
|
2121
|
+
` : ""}
|
|
2122
|
+
${truncatedContent}`;
|
|
2123
|
+
}).join("\n\n---\n\n");
|
|
2124
|
+
styleExamples = `<published_essays>
|
|
2125
|
+
The following are examples of the author's published work. Use these to match their voice, tone, and writing style:
|
|
2126
|
+
|
|
2127
|
+
${examples}
|
|
2128
|
+
</published_essays>`;
|
|
2129
|
+
}
|
|
2130
|
+
} catch (err) {
|
|
2131
|
+
console.error("[AI Chat] Failed to fetch published essays:", err);
|
|
2132
|
+
}
|
|
1400
2133
|
const { chatStream: chatStream2 } = await Promise.resolve().then(() => (init_chat(), chat_exports));
|
|
1401
2134
|
try {
|
|
1402
2135
|
const stream = await chatStream2({
|
|
@@ -1405,9 +2138,18 @@ async function handleAIAPI(req, cms, session, path) {
|
|
|
1405
2138
|
essayContext,
|
|
1406
2139
|
mode,
|
|
1407
2140
|
chatRules: settings.chatRules,
|
|
2141
|
+
rules: settings.rules,
|
|
1408
2142
|
template: settings.chatTemplate,
|
|
1409
|
-
|
|
1410
|
-
|
|
2143
|
+
// Plan mode specific settings
|
|
2144
|
+
planTemplate: settings.planTemplate,
|
|
2145
|
+
planRules: settings.planRules,
|
|
2146
|
+
// Agent mode specific settings
|
|
2147
|
+
agentTemplate: settings.agentTemplate,
|
|
2148
|
+
styleExamples,
|
|
2149
|
+
anthropicKey,
|
|
2150
|
+
openaiKey,
|
|
2151
|
+
useWebSearch,
|
|
2152
|
+
useThinking
|
|
1411
2153
|
});
|
|
1412
2154
|
return new Response(stream, {
|
|
1413
2155
|
headers: {
|
|
@@ -1417,11 +2159,89 @@ async function handleAIAPI(req, cms, session, path) {
|
|
|
1417
2159
|
}
|
|
1418
2160
|
});
|
|
1419
2161
|
} catch (error) {
|
|
2162
|
+
console.error("[AI Chat Error]", error);
|
|
1420
2163
|
return jsonResponse2({
|
|
1421
2164
|
error: error instanceof Error ? error.message : "Chat failed"
|
|
1422
2165
|
}, 500);
|
|
1423
2166
|
}
|
|
1424
2167
|
}
|
|
2168
|
+
if (method === "POST" && path === "/ai/rewrite") {
|
|
2169
|
+
const body = await req.json();
|
|
2170
|
+
const { text } = body;
|
|
2171
|
+
if (!text || typeof text !== "string") {
|
|
2172
|
+
return jsonResponse2({ error: "Text is required" }, 400);
|
|
2173
|
+
}
|
|
2174
|
+
const settings = await cms.aiSettings.get();
|
|
2175
|
+
const anthropicKey = cms.config.ai?.anthropicKey || settings.anthropicKey;
|
|
2176
|
+
const openaiKey = cms.config.ai?.openaiKey || settings.openaiKey;
|
|
2177
|
+
let styleExamples = "";
|
|
2178
|
+
try {
|
|
2179
|
+
const publishedPosts = await cms.posts.findPublished();
|
|
2180
|
+
const MAX_STYLE_EXAMPLES = 3;
|
|
2181
|
+
const MAX_WORDS_PER_EXAMPLE = 300;
|
|
2182
|
+
if (publishedPosts.length > 0) {
|
|
2183
|
+
const examples = publishedPosts.slice(0, MAX_STYLE_EXAMPLES).map((post) => {
|
|
2184
|
+
const words = post.markdown.split(/\s+/);
|
|
2185
|
+
const truncatedContent = words.length > MAX_WORDS_PER_EXAMPLE ? words.slice(0, MAX_WORDS_PER_EXAMPLE).join(" ") + "..." : post.markdown;
|
|
2186
|
+
return `## ${post.title}
|
|
2187
|
+
${truncatedContent}`;
|
|
2188
|
+
}).join("\n\n---\n\n");
|
|
2189
|
+
styleExamples = examples;
|
|
2190
|
+
}
|
|
2191
|
+
} catch (err) {
|
|
2192
|
+
console.error("[AI Rewrite] Failed to fetch published essays:", err);
|
|
2193
|
+
}
|
|
2194
|
+
const { buildRewritePrompt: buildRewritePrompt2 } = await Promise.resolve().then(() => (init_builders(), builders_exports));
|
|
2195
|
+
const { createStream: createStream2 } = await Promise.resolve().then(() => (init_provider(), provider_exports));
|
|
2196
|
+
try {
|
|
2197
|
+
const systemPrompt = buildRewritePrompt2({
|
|
2198
|
+
rewriteRules: settings.rewriteRules,
|
|
2199
|
+
rules: settings.rules,
|
|
2200
|
+
template: settings.rewriteTemplate,
|
|
2201
|
+
styleExamples
|
|
2202
|
+
});
|
|
2203
|
+
const stream = await createStream2({
|
|
2204
|
+
model: settings.defaultModel,
|
|
2205
|
+
messages: [
|
|
2206
|
+
{ role: "system", content: systemPrompt },
|
|
2207
|
+
{ role: "user", content: `Rewrite the following text, preserving meaning but improving clarity and style:
|
|
2208
|
+
|
|
2209
|
+
${text}` }
|
|
2210
|
+
],
|
|
2211
|
+
anthropicKey,
|
|
2212
|
+
openaiKey,
|
|
2213
|
+
maxTokens: 2048
|
|
2214
|
+
});
|
|
2215
|
+
const reader = stream.getReader();
|
|
2216
|
+
const decoder = new TextDecoder();
|
|
2217
|
+
let rewrittenText = "";
|
|
2218
|
+
while (true) {
|
|
2219
|
+
const { done, value } = await reader.read();
|
|
2220
|
+
if (done) break;
|
|
2221
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
2222
|
+
const lines = chunk.split("\n");
|
|
2223
|
+
for (const line of lines) {
|
|
2224
|
+
if (line.startsWith("data: ")) {
|
|
2225
|
+
const data = line.slice(6);
|
|
2226
|
+
if (data === "[DONE]") continue;
|
|
2227
|
+
try {
|
|
2228
|
+
const parsed = JSON.parse(data);
|
|
2229
|
+
if (parsed.text) {
|
|
2230
|
+
rewrittenText += parsed.text;
|
|
2231
|
+
}
|
|
2232
|
+
} catch {
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
return jsonResponse2({ text: rewrittenText.trim() });
|
|
2238
|
+
} catch (error) {
|
|
2239
|
+
console.error("[AI Rewrite Error]", error);
|
|
2240
|
+
return jsonResponse2({
|
|
2241
|
+
error: error instanceof Error ? error.message : "Rewrite failed"
|
|
2242
|
+
}, 500);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
1425
2245
|
return jsonResponse2({ error: "Not found" }, 404);
|
|
1426
2246
|
}
|
|
1427
2247
|
|
|
@@ -1570,7 +2390,8 @@ async function handleSettingsAPI(req, cms, session, path) {
|
|
|
1570
2390
|
});
|
|
1571
2391
|
return jsonResponse2({
|
|
1572
2392
|
data: {
|
|
1573
|
-
autoDraftEnabled: integrationSettings?.autoDraftEnabled ?? false
|
|
2393
|
+
autoDraftEnabled: integrationSettings?.autoDraftEnabled ?? false,
|
|
2394
|
+
postUrlPattern: integrationSettings?.postUrlPattern ?? "/e/{slug}"
|
|
1574
2395
|
}
|
|
1575
2396
|
});
|
|
1576
2397
|
}
|
|
@@ -1578,11 +2399,18 @@ async function handleSettingsAPI(req, cms, session, path) {
|
|
|
1578
2399
|
const adminError = requireAdmin(cms, session);
|
|
1579
2400
|
if (adminError) return adminError;
|
|
1580
2401
|
const body = await req.json();
|
|
2402
|
+
const updateData = {};
|
|
1581
2403
|
if (typeof body.autoDraftEnabled === "boolean") {
|
|
2404
|
+
updateData.autoDraftEnabled = body.autoDraftEnabled;
|
|
2405
|
+
}
|
|
2406
|
+
if (typeof body.postUrlPattern === "string") {
|
|
2407
|
+
updateData.postUrlPattern = body.postUrlPattern;
|
|
2408
|
+
}
|
|
2409
|
+
if (Object.keys(updateData).length > 0) {
|
|
1582
2410
|
await prisma.integrationSettings.upsert({
|
|
1583
2411
|
where: { id: "default" },
|
|
1584
|
-
create: { id: "default",
|
|
1585
|
-
update:
|
|
2412
|
+
create: { id: "default", ...updateData },
|
|
2413
|
+
update: updateData
|
|
1586
2414
|
});
|
|
1587
2415
|
}
|
|
1588
2416
|
const integrationSettings = await prisma.integrationSettings.findUnique({
|
|
@@ -1590,7 +2418,8 @@ async function handleSettingsAPI(req, cms, session, path) {
|
|
|
1590
2418
|
});
|
|
1591
2419
|
return jsonResponse2({
|
|
1592
2420
|
data: {
|
|
1593
|
-
autoDraftEnabled: integrationSettings?.autoDraftEnabled ?? false
|
|
2421
|
+
autoDraftEnabled: integrationSettings?.autoDraftEnabled ?? false,
|
|
2422
|
+
postUrlPattern: integrationSettings?.postUrlPattern ?? "/e/{slug}"
|
|
1594
2423
|
}
|
|
1595
2424
|
});
|
|
1596
2425
|
}
|
|
@@ -6067,8 +6896,12 @@ function scrollToComment(editor, commentId) {
|
|
|
6067
6896
|
DEFAULT_REWRITE_TEMPLATE,
|
|
6068
6897
|
addCommentMark,
|
|
6069
6898
|
applyCommentMarks,
|
|
6899
|
+
buildAutoDraftPrompt,
|
|
6070
6900
|
buildChatPrompt,
|
|
6901
|
+
buildExpandPlanPrompt,
|
|
6071
6902
|
buildGeneratePrompt,
|
|
6903
|
+
buildPlanPrompt,
|
|
6904
|
+
buildRewritePrompt,
|
|
6072
6905
|
canDeleteComment,
|
|
6073
6906
|
canEditComment,
|
|
6074
6907
|
createAPIHandler,
|