1
0
mirror of https://github.com/typemill/typemill.git synced 2025-08-01 20:00:37 +02:00

V2.17 finish tonality and example articles

This commit is contained in:
trendschau
2025-05-17 13:03:17 +02:00
parent 1b776d170b
commit dd4875678f
4 changed files with 225 additions and 169 deletions

2
cache/timer.yaml vendored
View File

@@ -1 +1 @@
licenseupdate: 1747254971
licenseupdate: 1747478031

View File

@@ -4,46 +4,52 @@ promptlist:
content: 'Act as an experienced copywriter and brainstorm 10 compelling article ideas based on the provided topic. The goal is to create content that resonates with the target audience and ranks well on search engines. Identify relevant keywords and trends to align with user interests. Each idea should provide valuable insights, answer common questions, or solve specific problems. Craft engaging headlines that capture attention and match user search intent.'
active: true
system: true
link: null
outline:
title: outline
content: "You are an experienced copywriter skilled in crafting compelling articles tailored to a specific target audience. Conduct in-depth research on the following article idea. Identify the most relevant information, authoritative sources, and key insights. Analyze search intent to determine what readers are looking for when they search for this topic. Define the articles target audience, ideal length, tone, and structure.\n\nBased on your research, create a detailed structured outline for the article, including:\n\n* A compelling title and meta description.\n* Main headlines (H2, H3) that logically structure the content.\n* Bullet points under each headline, summarizing the key points to be covered.\n\nAfter the outline add a divider list, the headline `Research` and add your research findings."
active: true
system: true
link: null
outline2:
title: outline2
content: "You are an experienced copywriter skilled in crafting compelling, well-structured articles tailored to a specific target audience. Based on the provided article idea, develop a comprehensive content outline that includes:\n\n- Title: A compelling, SEO-optimized headline.\n- User Intent: A one-sentence description of the intent, followed by the category in brackets (Informational, Navigational, Transactional, or Commercial).\n- Keywords: A comma-separated list, with the main keyword first, followed by up to five secondary keywords.\n- Target Audience: One sentence who the article is for and what they expect to gain.\n- Ideal Tone & Style: Concise, authoritative, engaging, personal, etc.\n\nBelow that, provide a detailed article structure:\n- Headings (H2, H3): A logical outline that organizes the content.\n- Bullet points under each heading: Summarizing key points to cover."
active: true
system: true
link: null
write:
title: write
content: "Act as an experienced copywriter tasked with creating a comprehensive and engaging article based on the provided outline or draft. If details such as keywords, user intent, target audience, tone, or style are included, ensure the article aligns with them. If no outline is given, structure the article logically based on best practices for the topic.\n\nCraft a compelling article with:\n- **An eye-catching, SEO-optimized title** featuring the primary keyword.\n- **A strong introduction** that immediately hooks the reader—use a compelling question, statistic, expert quote, or relatable scenario. Clearly state what the article will cover.\n- **A well-structured body** with clear subheadings (H2, H3) incorporating relevant keywords. Ensure smooth transitions between sections and provide thorough, engaging explanations.\n- **Bullet points or lists** where appropriate to enhance readability but prioritize a natural flow of ideas.\n- **A unique perspective** by integrating expert insights, statistics, case studies, or real-life examples.\n- **Natural keyword integration** that enhances SEO without disrupting readability.\n\nMaintain a tone and style that align with the articles goals and audience, ensuring clarity, engagement, and credibility."
active: true
system: true
link: null
proofread:
title: proofread
content: 'Review the text for grammar, spelling, and correct word usage. Correct any errors and ensure that the appropriate words are used, while maintaining the original tone, style, and phrasing. At the end of the article, provide a list of all corrections made.'
active: true
system: true
link: null
refine:
title: refine
content: 'Refine the text to enhance its wording and style. Ensure that the content is readable, clear, concise, and flows naturally. Feel free to rephrase sentences where necessary, but retain the original tone and unique language usage to avoid making the text sound generic.'
active: true
system: true
link: null
review:
title: review
content: 'Review the article below. Evaluate it based on readability, clarity, SEO best practices, engagement, and search intent. Provide actionable suggestions for improvement, focusing on areas where the text can be refined for better flow, clarity, and alignment with SEO goals.'
active: true
system: true
link: null
mermaid:
title: mermaid
content: 'Return pure mermaid syntax for a [pie, x, y] diagram.'
active: true
system: true
example:
title: example
content: 'Please use the following and write an article.'
link: null
tonality:
title: tonality
content: 'You are an experienced copywriter. Your task is to take the tone from the example and apply them to the article. Tone includes formality or informality, sentence rhythm, word choice, emotional undertone, and stylistic flair. Keep the original structure, content, and key messages of the article intact, but rewrite it in a way that it reads as if written by the same author as the example. The result should preserve the meaning of the article while fully reflecting the tone and writing style of the example.'
active: true
system: false
errors:
title: false
body: false
link: /getting-started/edit-your-page

View File

@@ -5,8 +5,6 @@ namespace Typemill\Controllers;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Typemill\Models\Validation;
use Typemill\Models\Navigation;
use Typemill\Models\Content;
use Typemill\Models\License;
use Typemill\Models\Settings;
use Typemill\Models\User;
@@ -46,7 +44,7 @@ class ControllerApiKixote extends Controller
# send to Kixote
$response->getBody()->write(json_encode([
'settings' => $kixoteSettings
'kixotesettings' => $kixoteSettings
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
@@ -284,31 +282,35 @@ class ControllerApiKixote extends Controller
$promptname = $params['name'] ?? '';
$prompt = $params['prompt'] ?? '';
$article = $params['article'] ?? '';
$example = $params['link'] ?? false;
if($example)
$example = $params['example'] ?? false;
if($example && $example != "")
{
$validate = new Validation();
$validInput = $validate->articleUrl(['url' => $params['link']]);
if($validInput === true)
$validation = new Validation();
$v = $validation->returnValidator(['content' => $example]);
$v->rule('markdownSecure', 'content');
if(!$v->validate())
{
$urlinfo = $this->c->get('urlinfo');
$langattr = $this->settings['langattr'];
$navigation = new Navigation();
$item = $navigation->getItemForUrl($params['url'], $urlinfo, $langattr);
if($item)
$example = false;
}
else
{
# Rough estimate: 1 token ≈ 4 characters
$allContent = $prompt . $article . $example;
$length = strlen($allContent);
$maxlength = 8000 * 4;
if ($length > $maxlength)
{
$content = new Content($urlinfo['baseurl'], $this->settings, $this->c->get('dispatcher'));
$markdown = $content->getDraftMarkdown($item);
if($markdown)
{
if(is_array($markdown))
{
$markdown = $content->markdownArrayToText($markdown);
}
$example = $markdown;
}
$overLimit = $length - $maxlength;
$keep = strlen($example) - $overLimit;
if($keep > 0)
{
$example = substr($example, 0, $keep);
}
else
{
$example = false;
}
}
}
}
@@ -370,12 +372,11 @@ class ControllerApiKixote extends Controller
$url = 'https://api.openai.com/v1/chat/completions';
$authHeader = "Authorization: Bearer $apikey";
$content = $prompt;
$content = $prompt . "\n<article>" . $article . "<article>";
if($example)
{
$content .= "\n<example>" . $example . "</example>";
}
$content .= "\n<article>" . $article . "<article>";
$postdata = [
'model' => $model,
@@ -446,12 +447,11 @@ class ControllerApiKixote extends Controller
"anthropic-version: 2023-06-01"
];
$content = $prompt;
$content = $prompt . "\n<article>" . $article . "<article>";
if($example)
{
$content .= "\n<example>" . $example . "</example>";
}
$content .= "\n<article>" . $article . "<article>";
$postdata = [
'model' => $model,

View File

@@ -189,6 +189,32 @@ const kixote = Vue.createApp({
}
},
methods: {
setKixoteSettings(rawSettings)
{
// Clone to avoid mutating original input
const normalizedSettings = rawSettings;
if (
normalizedSettings &&
normalizedSettings.promptlist
)
{
const promptlist = normalizedSettings.promptlist;
for (const key in promptlist)
{
if (promptlist.hasOwnProperty(key))
{
if (typeof promptlist[key].link === 'undefined')
{
promptlist[key].link = null;
}
}
}
}
this.kixoteSettings = normalizedSettings;
},
loadKixoteSettings()
{
self = this;
@@ -200,9 +226,9 @@ const kixote = Vue.createApp({
})
.then(function (response)
{
if (response.data.settings)
if (response.data.kixotesettings)
{
self.kixoteSettings = response.data.settings;
self.setKixoteSettings(response.data.kixotesettings);
}
})
.catch(function (error)
@@ -294,7 +320,13 @@ const kixote = Vue.createApp({
updateKixoteSettings(newSettings)
{
this.settingsSaved = false;
this.kixoteSettings = { ...this.kixoteSettings, ...newSettings }; // ✅ Merge settings
this.kixoteSettings = newSettings;
/*
this.kixoteSettings = { ...this.kixoteSettings, ...newSettings };
console.info("this.settings after merge");
console.info(this.kixoteSettings);
*/
},
storeKixoteSettings()
{
@@ -307,13 +339,13 @@ const kixote = Vue.createApp({
.then(function (response)
{
self.settingsSaved = true;
self.kixoteSettings = response.data.kixotesettings;
self.setKixoteSettings(response.data.kixotesettings);
})
.catch(function (error)
{
if(error.response)
{
self.kixoteSettings = error.response.data.kixotesettings;
self.setKixoteSettings(error.response.data.kixotesettings);
}
});
}
@@ -333,7 +365,8 @@ kixote.component('tab-generate', {
activeversion: 0,
versions: [],
prompt: '',
promptlink: null,
promptlink: false,
examplecontent: false,
promptError: false,
showFocusButton: false,
buttonPosition: { top: 0, left: 0 },
@@ -349,7 +382,7 @@ kixote.component('tab-generate', {
},
titleError: false,
bodyError: false,
currentFilter: 'all',
currentFilter: 'user',
article: '',
index: '',
flatnavi: false,
@@ -465,14 +498,17 @@ kixote.component('tab-generate', {
<div class="w-full bg-stone-600 px-8 py-2">
<div class="flex items-start">
<span class="text-teal-300 mr-1">Ki></span>
<textarea
v-model.trim = "prompt"
ref = "prompteditor"
class = "flex-grow bg-stone-600 focus:outline-none border-0 caret-white"
placeholder = "Prompt..."
@keydown.enter = "handleKeydown"
@input = "resizePromptEditor"
></textarea>
<div class="flex-grow bg-stone-600">
<textarea
v-model.trim = "prompt"
ref = "prompteditor"
class = "w-full bg-stone-600 focus:outline-none border-0 caret-white"
placeholder = "Prompt..."
@keydown.enter = "handleKeydown"
@input = "resizePromptEditor"
></textarea>
<p class="" v-if="promptlink">Example: {{promptlink}}</p>
</div>
<button
class = "text-teal-300 px-2"
@click = "submitPrompt"
@@ -516,13 +552,6 @@ kixote.component('tab-generate', {
</button>
<div class="flex space-x-2">
<span class="px-1">Filter:</span>
<button
@click.prevent="currentFilter = 'system'"
:class="{'text-teal-500': currentFilter === 'system', 'text-white': currentFilter !== 'system'}"
class="px-1 transition-colors"
>
system prompts
</button>
<button
@click.prevent="currentFilter = 'user'"
:class="{'text-teal-500': currentFilter === 'user', 'text-white': currentFilter !== 'user'}"
@@ -530,6 +559,13 @@ kixote.component('tab-generate', {
>
my prompts
</button>
<button
@click.prevent="currentFilter = 'system'"
:class="{'text-teal-500': currentFilter === 'system', 'text-white': currentFilter !== 'system'}"
class="px-1 transition-colors"
>
system prompts
</button>
</div>
</div>
@@ -540,7 +576,7 @@ kixote.component('tab-generate', {
<input
type = "text"
class = "w-50 p-2 my-1 font-mono bg-stone-600 text-white caret-white focus:outline-none"
@input = "validateTitle(newPrompt.title)"
@input = "validatePromptTitle(newPrompt.title)"
@focus = "editPrompt = newPrompt.title"
placeholder = "Enter a title"
v-model = "newPrompt.title"
@@ -550,7 +586,7 @@ kixote.component('tab-generate', {
<textarea
class = "w-full p-2 my-1 font-mono bg-stone-600 no-outline text-white caret-white focus:outline-none"
rows = "5"
@input = "validateBody(newPrompt.content)"
@input = "validatePromptBody(newPrompt.content)"
@focus = "editPrompt = newPrompt.name"
placeholder = "Enter a prompt"
v-model = "newPrompt.content"
@@ -562,8 +598,8 @@ kixote.component('tab-generate', {
<select v-model="newPrompt.link"
class="w-full p-2 font-mono bg-stone-600 text-white caret-white focus:outline-none">
<option :value="null" class="text-stone-400 italic">Select example article</option>
<option v-for="naviitem in flatnavi" :key="naviitem.urlWoF" :value="naviitem.urlRelWoF">
{{ naviitem }}
<option v-for="navilink in flatnavi" :key="navilink" :value="navilink">
{{ navilink }}
</option>
</select>
</div>
@@ -577,11 +613,26 @@ kixote.component('tab-generate', {
</div>
</transition>
<div
v-for = "(prompttemplate, name) in filteredPrompts"
:key="name"
v-if = "currentFilter == 'user' && Object.keys(filteredPrompts).length === 0"
class = "py-2 px-2"
>
<div class="border border-stone-700 bg-stone-700 text-stone-100 p-4">
<h2 class="text-lg font-semibold mb-2">How to Use</h2>
<p class="mb-2">Click the <span class="font-medium">+ Add Prompt</span> button to create your own prompts. A custom prompt can include:</p>
<ul class="list-disc list-inside mb-4 space-y-1">
<li>A title or name</li>
<li>The main prompt text</li>
<li>An optional link to an article used as example (e.g. for style or tone)</li>
<li>An activation checkbox to show or hide the prompt below the prompt input field</li>
</ul>
<p>You can also browse the predefined system prompts using the filter above. These cannot be edited, but you can activate or deactivate them as needed.</p>
</div>
</div>
<div
v-for = "(prompttemplate, name) in filteredPrompts"
:key = "name"
class = "py-2 px-2"
>
{{ prompttemplate }}
<fieldset class="border border-stone-700 p-4">
<div class="flex w-full justify-between">
<input
@@ -590,7 +641,7 @@ kixote.component('tab-generate', {
:readonly = "prompttemplate.system"
v-model = "prompttemplate.title"
@focus = "editPrompt = name"
@input = "updatePrompt(kixoteSettings, name)"
@input = "updatePrompt(name)"
/>
<div class="flex space-x-2 items-center">
<div v-if="prompttemplate.system == false">
@@ -606,7 +657,7 @@ kixote.component('tab-generate', {
</span>
<button
v-else
@click.prevent="savePrompts"
@click.prevent="saveSettings"
class="px-1 text-teal-300 hover:text-teal-500 transition-colors"
>update
</button>
@@ -617,7 +668,7 @@ kixote.component('tab-generate', {
type = "checkbox"
class = "w-5 h-5 border border-stone-300 bg-stone-600 text-white cursor-pointer"
v-model = "prompttemplate.active"
@change = "updateSettings(kixoteSettings)"
@change = "saveSettings"
>
</div>
</div>
@@ -629,17 +680,17 @@ kixote.component('tab-generate', {
v-model = "prompttemplate.content"
:readonly = "prompttemplate.system"
@focus = "editPrompt = name"
@input = "updatePrompt(kixoteSettings, name)"
@input = "updatePrompt(name)"
>
</textarea>
<span v-if="prompttemplate.errors?.body" class="text-red-500 text-sm">{{ prompttemplate.errors.body }}</span>
<div class="space-y-2 my-2">
<div class="space-y-2 my-2" v-if="prompttemplate.system !== true">
<select v-model="prompttemplate.link"
class="w-full p-2 font-mono bg-stone-600 text-white caret-white focus:outline-none">
<option :value="null">Select example article</option>
<option v-for="naviitem in flatnavi" :key="naviitem.urlWoF" :value="naviitem.urlRelWoF">
{{ naviitem }}
<option v-for="navilink in flatnavi" :key="navilink" :value="navilink">
{{ navilink }}
</option>
</select>
</div>
@@ -649,57 +700,6 @@ kixote.component('tab-generate', {
</div>
</div>
<div v-else-if="currentTab === 'tone'">
<div class="w-full bg-stone-900 px-8 py-8">
<div class="flex justify-between py-2 px-2">
<button @click.prevent="addNewTone = !addNewTone">
<span v-if="addNewTone">-</span>
<span v-else>+</span> add tone
</button>
</div>
<transition name="fade">
<div v-if="addNewTone" class="py-2 px-2">
<fieldset class="border border-stone-700 p-4">
<input
type="text"
class="w-50 p-2 my-1 font-mono bg-stone-600 text-white caret-white focus:outline-none"
placeholder="Enter a tone name"
v-model="newTone.title"
/>
<!-- Tone description result -->
<textarea
rows="5"
class="w-full p-2 my-1 font-mono bg-stone-600 text-white caret-white focus:outline-none"
placeholder="Tone description"
v-model="newTone.description"
></textarea>
<div class="w-full flex justify-between">
<!-- Button to analyze tone -->
<button
@click.prevent="analyzeTone(newTone)"
class="px-1 text-teal-300 hover:text-teal-500 transition-colors"
>analyze tone</button>
<!-- Save button -->
<button
@click.prevent="saveNewTone"
class="px-1 text-teal-300 hover:text-teal-500 transition-colors"
>save</button>
</div>
</fieldset>
</div>
</transition>
</div>
</div>
</div>
</div>
@@ -755,17 +755,7 @@ kixote.component('tab-generate', {
? this.promptlistuser
: this.kixoteSettings.promptlist;
// Normalize `link` to `null` if not present
const normalized = {};
for (const [key, prompt] of Object.entries(list))
{
normalized[key] = {
...prompt,
link: prompt.link ?? null
};
}
return normalized;
return list;
}
},
methods: {
@@ -820,6 +810,23 @@ kixote.component('tab-generate', {
this.versions.push(markdown);
this.resizeAiEditor();
},
setExampleContent(example)
{
let markdown = '';
if (Array.isArray(example))
{
for (const block of example)
{
if (block && block.markdown)
{
markdown += block.markdown + '\n\n';
}
}
}
this.examplecontent = markdown.trim();
},
createFlatNavi() {
if (this.navigation && !this.flatnavi) {
const nestedNavi = [];
@@ -863,6 +870,7 @@ kixote.component('tab-generate', {
{
this.prompt = this.promptlistactive[index].content;
this.promptlink = this.promptlistactive[index].link;
this.examplecontent = false;
this.resizePromptEditor();
},
switchVersion(index)
@@ -874,13 +882,19 @@ kixote.component('tab-generate', {
{
this.promptError = false;
if (this.promptlink && this.examplecontent === false)
{
this.loadExampleContent();
return;
}
var self = this;
eventBus.$emit('switchLoading');
tmaxios.post('/api/v1/prompt',{
'prompt': this.prompt,
'article': this.versions[this.activeversion],
'link': this.promptlink
'example': this.examplecontent
})
.then(function (response)
{
@@ -893,6 +907,7 @@ kixote.component('tab-generate', {
self.activeversion = self.versions.length-1;
self.prompt = '';
self.promptlink = null;
self.examplecontent = false;
self.resizePromptEditor();
self.resizeAiEditor();
}
@@ -912,6 +927,34 @@ kixote.component('tab-generate', {
}
});
},
loadExampleContent()
{
self = this;
this.examplecontent = '';
tmaxios.get('/api/v1/article/content',{
params: {
'url': this.promptlink,
'draft': true
}
})
.then(function (response)
{
if (response.data.content)
{
self.setExampleContent(response.data.content);
}
self.submitPrompt(true);
})
.catch(function (error)
{
if(error.response)
{
}
self.submitPrompt(true);
});
},
handleKeydown(event)
{
if (event.key === 'Enter' && !event.shiftKey)
@@ -1015,11 +1058,7 @@ kixote.component('tab-generate', {
.replace(/-+/g, '-') // Remove multiple dashes
.trim(); // Trim leading/trailing dashes
},
updateSettings(newSettings)
{
eventBus.$emit('updateKixoteSettings', newSettings); // Emit event
},
validateTitle(title)
validatePromptTitle(title)
{
const titleRegex = /^[a-zA-Z0-9 ]{0,20}$/;
if (!titleRegex.test(title))
@@ -1031,7 +1070,7 @@ kixote.component('tab-generate', {
this.titleError = false;
}
},
validateBody(body)
validatePromptBody(body)
{
const bodyRegex = /<\/?[^>]+(>|$)/g;
if (bodyRegex.test(body))
@@ -1042,36 +1081,36 @@ kixote.component('tab-generate', {
{
this.bodyError = false;
}
},
savePrompts()
},
updatePrompt(name)
{
if (!this.titleError && !this.bodyError)
var newSettings = this.kixoteSettings;
if(newSettings.promptlist[name] != undefined)
{
eventBus.$emit('storeKixoteSettings');
}
},
updatePrompt(kixoteSettings, promptname)
{
if(kixoteSettings.promptlist[promptname] != undefined)
{
kixoteSettings.promptlist[promptname].errors = {};
newSettings.promptlist[name].errors = {};
this.validateTitle(kixoteSettings.promptlist[promptname].title);
kixoteSettings.promptlist[promptname].errors.title = this.titleError;
this.validatePromptTitle(newSettings.promptlist[name].title);
if(this.titleError)
{
newSettings.promptlist[name].errors.title = this.titleError;
}
this.validateBody(kixoteSettings.promptlist[promptname].content);
kixoteSettings.promptlist[promptname].errors.body = this.bodyError;
this.validatePromptBody(newSettings.promptlist[name].content);
if(this.bodyError)
{
newSettings.promptlist[name].errors.body = this.bodyError;
}
this.updateSettings(kixoteSettings);
this.updateSettings(newSettings);
}
},
deletePrompt(name)
{
var promptlist = this.kixoteSettings.promptlist;
var newSettings = this.kixoteSettings;
delete promptlist[name];
delete newSettings.promptlist[name];
this.updateSettings(promptlist);
this.updateSettings(newSettings);
eventBus.$emit('storeKixoteSettings');
@@ -1083,30 +1122,41 @@ kixote.component('tab-generate', {
return false;
}
var promptlist = this.kixoteSettings.promptlist;
var newSettings = this.kixoteSettings;
var promptkey = this.slugify(this.newPrompt.title);
promptlist[promptkey] = {
newSettings.promptlist[promptkey] = {
title: this.newPrompt.title,
content: this.newPrompt.content,
active: this.newPrompt.active,
system: this.newPrompt.system
system: this.newPrompt.system,
link: this.newPrompt.link
};
this.newPrompt = {
title: '',
content: '',
active: true,
system: false
system: false,
link: null
};
this.addNewPrompt = false;
this.updateSettings(promptlist);
this.updateSettings(newSettings);
eventBus.$emit('storeKixoteSettings');
},
getArticleMarkdown(url)
{
},
updateSettings(newSettings)
{
eventBus.$emit('updateKixoteSettings', newSettings);
},
saveSettings()
{
/* used if activate box for prompts is clicked */
if (!this.titleError && !this.bodyError)
{
eventBus.$emit('storeKixoteSettings');
}
},
exit()
{
eventBus.$emit('kiExit');