forked from open-webui/open-webui
Merge pull request #349 from ollama-webui/prompts
feat: prompt preset support
This commit is contained in:
commit
14efed3651
4 changed files with 339 additions and 31 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -24,7 +24,6 @@ dist/
|
||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
lib/
|
|
||||||
lib64/
|
lib64/
|
||||||
parts/
|
parts/
|
||||||
sdist/
|
sdist/
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { settings } from '$lib/stores';
|
|
||||||
import toast from 'svelte-french-toast';
|
import toast from 'svelte-french-toast';
|
||||||
|
import { onMount, tick } from 'svelte';
|
||||||
|
import { settings } from '$lib/stores';
|
||||||
|
import { findWordIndices } from '$lib/utils';
|
||||||
|
|
||||||
|
import Prompts from './MessageInput/PromptCommands.svelte';
|
||||||
import Suggestions from './MessageInput/Suggestions.svelte';
|
import Suggestions from './MessageInput/Suggestions.svelte';
|
||||||
import { onMount } from 'svelte';
|
|
||||||
|
|
||||||
export let submitPrompt: Function;
|
export let submitPrompt: Function;
|
||||||
export let stopResponse: Function;
|
export let stopResponse: Function;
|
||||||
|
@ -11,6 +14,8 @@
|
||||||
export let autoScroll = true;
|
export let autoScroll = true;
|
||||||
|
|
||||||
let filesInputElement;
|
let filesInputElement;
|
||||||
|
let promptsElement;
|
||||||
|
|
||||||
let inputFiles;
|
let inputFiles;
|
||||||
let dragged = false;
|
let dragged = false;
|
||||||
|
|
||||||
|
@ -154,36 +159,42 @@
|
||||||
|
|
||||||
<div class="fixed bottom-0 w-full">
|
<div class="fixed bottom-0 w-full">
|
||||||
<div class="px-2.5 pt-2.5 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
|
<div class="px-2.5 pt-2.5 -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
|
||||||
{#if messages.length == 0 && suggestionPrompts.length !== 0}
|
<div class="flex flex-col max-w-3xl w-full">
|
||||||
<div class="max-w-3xl w-full">
|
<div>
|
||||||
<Suggestions {suggestionPrompts} {submitPrompt} />
|
{#if autoScroll === false && messages.length > 0}
|
||||||
|
<div class=" flex justify-center mb-4">
|
||||||
|
<button
|
||||||
|
class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full"
|
||||||
|
on:click={() => {
|
||||||
|
autoScroll = true;
|
||||||
|
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-5 h-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M10 3a.75.75 0 01.75.75v10.638l3.96-4.158a.75.75 0 111.08 1.04l-5.25 5.5a.75.75 0 01-1.08 0l-5.25-5.5a.75.75 0 111.08-1.04l3.96 4.158V3.75A.75.75 0 0110 3z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if autoScroll === false && messages.length > 0}
|
<div class="w-full">
|
||||||
<div class=" flex justify-center mb-4">
|
{#if prompt.charAt(0) === '/'}
|
||||||
<button
|
<Prompts bind:this={promptsElement} bind:prompt />
|
||||||
class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full"
|
{:else if messages.length == 0 && suggestionPrompts.length !== 0}
|
||||||
on:click={() => {
|
<Suggestions {suggestionPrompts} {submitPrompt} />
|
||||||
autoScroll = true;
|
{/if}
|
||||||
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M10 3a.75.75 0 01.75.75v10.638l3.96-4.158a.75.75 0 111.08 1.04l-5.25 5.5a.75.75 0 01-1.08 0l-5.25-5.5a.75.75 0 111.08-1.04l3.96 4.158V3.75A.75.75 0 0110 3z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white dark:bg-gray-800">
|
<div class="bg-white dark:bg-gray-800">
|
||||||
<div class="max-w-3xl px-2.5 -mb-0.5 mx-auto inset-x-0">
|
<div class="max-w-3xl px-2.5 -mb-0.5 mx-auto inset-x-0">
|
||||||
|
@ -298,7 +309,7 @@
|
||||||
submitPrompt(prompt);
|
submitPrompt(prompt);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
on:keydown={(e) => {
|
on:keydown={async (e) => {
|
||||||
if (prompt === '' && e.key == 'ArrowUp') {
|
if (prompt === '' && e.key == 'ArrowUp') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -315,6 +326,61 @@
|
||||||
userMessageElement.scrollIntoView({ block: 'center' });
|
userMessageElement.scrollIntoView({ block: 'center' });
|
||||||
editButton?.click();
|
editButton?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prompt.charAt(0) === '/' && e.key === 'ArrowUp') {
|
||||||
|
promptsElement.selectUp();
|
||||||
|
|
||||||
|
const commandOptionButton = [
|
||||||
|
...document.getElementsByClassName('selected-command-option-button')
|
||||||
|
]?.at(-1);
|
||||||
|
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prompt.charAt(0) === '/' && e.key === 'ArrowDown') {
|
||||||
|
promptsElement.selectDown();
|
||||||
|
|
||||||
|
const commandOptionButton = [
|
||||||
|
...document.getElementsByClassName('selected-command-option-button')
|
||||||
|
]?.at(-1);
|
||||||
|
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prompt.charAt(0) === '/' && e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const commandOptionButton = [
|
||||||
|
...document.getElementsByClassName('selected-command-option-button')
|
||||||
|
]?.at(-1);
|
||||||
|
|
||||||
|
commandOptionButton?.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prompt.charAt(0) === '/' && e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const commandOptionButton = [
|
||||||
|
...document.getElementsByClassName('selected-command-option-button')
|
||||||
|
]?.at(-1);
|
||||||
|
|
||||||
|
commandOptionButton?.click();
|
||||||
|
} else if (e.key === 'Tab') {
|
||||||
|
const words = findWordIndices(prompt);
|
||||||
|
|
||||||
|
if (words.length > 0) {
|
||||||
|
const word = words.at(0);
|
||||||
|
const fullPrompt = prompt;
|
||||||
|
|
||||||
|
prompt = prompt.substring(0, word?.endIndex + 1);
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
e.target.scrollTop = e.target.scrollHeight;
|
||||||
|
prompt = fullPrompt;
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.target.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
rows="1"
|
rows="1"
|
||||||
on:input={(e) => {
|
on:input={(e) => {
|
||||||
|
|
227
src/lib/components/chat/MessageInput/PromptCommands.svelte
Normal file
227
src/lib/components/chat/MessageInput/PromptCommands.svelte
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { findWordIndices } from '$lib/utils';
|
||||||
|
import { tick } from 'svelte';
|
||||||
|
|
||||||
|
export let prompt = '';
|
||||||
|
|
||||||
|
let selectedCommandIdx = 0;
|
||||||
|
|
||||||
|
let promptCommands = [
|
||||||
|
{
|
||||||
|
command: '/article',
|
||||||
|
title: 'Article Generator',
|
||||||
|
content: `Write an article about [topic]
|
||||||
|
|
||||||
|
include relevant statistics (add the links of the sources you use) and consider diverse perspectives. Write it in a [X_tone] and mention the source links in the end.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: '/backlink',
|
||||||
|
|
||||||
|
title: 'Backlink Outreach Email',
|
||||||
|
content: `Write a link-exchange outreach email on behalf of [your name] from [your_company] to ask for a backlink from their [website_url] to [your website url].`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: '/faq',
|
||||||
|
|
||||||
|
title: 'FAQ Generator',
|
||||||
|
content: `Create a list of [10] frequently asked questions about [keyword] and provide answers for each one of them considering the SERP and rich result guidelines.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: '/headline',
|
||||||
|
|
||||||
|
title: 'Headline Generator',
|
||||||
|
content: `Generate 10 attention-grabbing headlines for an article about [your topic]`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: '/product',
|
||||||
|
|
||||||
|
title: 'Product Description',
|
||||||
|
content: `Craft an irresistible product description that highlights the benefits of [your product]`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: '/seo',
|
||||||
|
|
||||||
|
title: 'SEO Content Brief',
|
||||||
|
content: `Create a SEO content brief for [keyword].`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: '/seo-ideas',
|
||||||
|
|
||||||
|
title: 'SEO Keyword Ideas',
|
||||||
|
content: `Generate a list of 20 keyword ideas on [topic].
|
||||||
|
|
||||||
|
Cluster this list of keywords according to funnel stages whether they are top of the funnel, middle of the funnel or bottom of the funnel keywords.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: '/summary',
|
||||||
|
|
||||||
|
title: 'Short Summary',
|
||||||
|
content: `Write a summary in 50 words that summarizes [topic or keyword].`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: '/email-subject',
|
||||||
|
|
||||||
|
title: 'Email Subject Line',
|
||||||
|
content: `Develop [5] subject lines for a cold email offering your [product or service] to a potential client.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: '/facebook-ads',
|
||||||
|
|
||||||
|
title: 'Facebook Ads',
|
||||||
|
content: `Create 3 variations of effective ad copy to promote [product] for [audience].
|
||||||
|
|
||||||
|
Make sure they are [persuasive/playful/emotional] and mention these benefits:
|
||||||
|
|
||||||
|
[Benefit 1]
|
||||||
|
|
||||||
|
[Benefit 2]
|
||||||
|
|
||||||
|
[Benefit 3]
|
||||||
|
|
||||||
|
Finish with a call to action saying [CTA].
|
||||||
|
|
||||||
|
Add 3 emojis to it.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: '/google-ads',
|
||||||
|
|
||||||
|
title: 'Google Ads',
|
||||||
|
content: `Create 10 google ads (a headline and a description) for [product description] targeting the keyword [keyword].
|
||||||
|
|
||||||
|
The headline of the ad needs to be under 30 characters. The description needs to be under 90 characters. Format the output as a table.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: '/insta-caption',
|
||||||
|
|
||||||
|
title: 'Instagram Caption',
|
||||||
|
content: `Write 5 variations of Instagram captions for [product].
|
||||||
|
|
||||||
|
Use friendly, human-like language that appeals to [target audience].
|
||||||
|
|
||||||
|
Emphasize the unique qualities of [product],
|
||||||
|
|
||||||
|
use ample emojis, and don't sound too promotional.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: '/linkedin-post',
|
||||||
|
|
||||||
|
title: 'LinkedIn Post',
|
||||||
|
content: `Create a narrative Linkedin post using immersive writing about [topic].
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
[give details in bullet point format]
|
||||||
|
|
||||||
|
Use a mix of short and long sentences. Make it punchy and dramatic.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: '/youtube-desc',
|
||||||
|
|
||||||
|
title: 'YouTube Video',
|
||||||
|
content: `Write a 100-word YouTube video description that compels [audience]
|
||||||
|
|
||||||
|
to watch a video on [topic]
|
||||||
|
|
||||||
|
and mentions the following keywords
|
||||||
|
|
||||||
|
[keyword 1]
|
||||||
|
|
||||||
|
[keyword 2]
|
||||||
|
|
||||||
|
[keyword 3].`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: '/seo-meta',
|
||||||
|
|
||||||
|
title: 'SEO Meta',
|
||||||
|
content: `Suggest a meta description for the content above, make it user-friendly and with a call to action, include the keyword [keyword].`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: '/eli5',
|
||||||
|
|
||||||
|
title: 'ELI5',
|
||||||
|
content: `You are an expert teacher with the ability to explain complex topics in simpler terms. Explain the concept of [topic] in simple terms, so that my [grade level/subject] class can understand [this concept/specific example]?`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: '/emoji-translate',
|
||||||
|
|
||||||
|
title: 'Emoji Translation',
|
||||||
|
content: `You are an emoji expert. Using only emojis, translate the following text to emojis. [insert numbered sentences].`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let filteredPromptCommands = [];
|
||||||
|
|
||||||
|
$: filteredPromptCommands = promptCommands
|
||||||
|
.filter((p) => p.command.includes(prompt))
|
||||||
|
.sort((a, b) => a.title.localeCompare(b.title));
|
||||||
|
|
||||||
|
$: if (prompt) {
|
||||||
|
selectedCommandIdx = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selectUp = () => {
|
||||||
|
selectedCommandIdx = Math.max(0, selectedCommandIdx - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectDown = () => {
|
||||||
|
selectedCommandIdx = Math.min(selectedCommandIdx + 1, filteredPromptCommands.length - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmCommand = async (command) => {
|
||||||
|
prompt = command.content;
|
||||||
|
|
||||||
|
const chatInputElement = document.getElementById('chat-textarea');
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
chatInputElement.style.height = '';
|
||||||
|
chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px';
|
||||||
|
|
||||||
|
chatInputElement?.focus();
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
const words = findWordIndices(prompt);
|
||||||
|
|
||||||
|
if (words.length > 0) {
|
||||||
|
const word = words.at(0);
|
||||||
|
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if filteredPromptCommands.length > 0}
|
||||||
|
<div class="md:px-2 mb-3 text-left w-full">
|
||||||
|
<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700">
|
||||||
|
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-lg text-center">
|
||||||
|
<div class=" text-lg font-medium mt-2">/</div>
|
||||||
|
</div>
|
||||||
|
<div class=" max-h-60 overflow-y-auto bg-white w-full p-2 rounded-r-lg space-y-0.5">
|
||||||
|
{#each filteredPromptCommands as command, commandIdx}
|
||||||
|
<button
|
||||||
|
class=" px-3 py-1.5 rounded-lg w-full text-left {commandIdx === selectedCommandIdx
|
||||||
|
? ' bg-gray-100 selected-command-option-button'
|
||||||
|
: ''}"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
confirmCommand(command);
|
||||||
|
}}
|
||||||
|
on:mousemove={() => {
|
||||||
|
selectedCommandIdx = commandIdx;
|
||||||
|
}}
|
||||||
|
on:focus={() => {}}
|
||||||
|
>
|
||||||
|
<div class=" font-medium text-black">
|
||||||
|
{command.command}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" text-xs text-gray-600">
|
||||||
|
{command.title}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
|
@ -111,3 +111,19 @@ export const checkVersion = (required, current) => {
|
||||||
caseFirst: 'upper'
|
caseFirst: 'upper'
|
||||||
}) < 0;
|
}) < 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const findWordIndices = (text) => {
|
||||||
|
const regex = /\[([^\]]+)\]/g;
|
||||||
|
let matches = [];
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
matches.push({
|
||||||
|
word: match[1],
|
||||||
|
startIndex: match.index,
|
||||||
|
endIndex: regex.lastIndex - 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue