Merge pull request #3818 from gitbutlerapp/customizable-ai-prompts

Leeerrrooyyyy jeeennnkkkininnsss
This commit is contained in:
Caleb Owens 2024-05-31 22:02:31 +02:00 committed by GitHub
commit 9cde2babcb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 967 additions and 71 deletions

View File

@ -1,6 +1,6 @@
import { SHORT_DEFAULT_COMMIT_TEMPLATE, SHORT_DEFAULT_BRANCH_TEMPLATE } from '$lib/ai/prompts';
import { fetch, Body } from '@tauri-apps/api/http';
import type { AIClient, AnthropicModelName, PromptMessage } from '$lib/ai/types';
import type { AIClient, AnthropicModelName, Prompt } from '$lib/ai/types';
type AnthropicAPIResponse = { content: { text: string }[] };
@ -13,7 +13,7 @@ export class AnthropicAIClient implements AIClient {
private modelName: AnthropicModelName
) {}
async evaluate(prompt: PromptMessage[]) {
async evaluate(prompt: Prompt) {
const body = Body.json({
messages: prompt,
max_tokens: 1024,

View File

@ -1,5 +1,5 @@
import { SHORT_DEFAULT_BRANCH_TEMPLATE, SHORT_DEFAULT_COMMIT_TEMPLATE } from '$lib/ai/prompts';
import type { AIClient, ModelKind, PromptMessage } from '$lib/ai/types';
import type { AIClient, ModelKind, Prompt } from '$lib/ai/types';
import type { HttpClient } from '$lib/backend/httpClient';
export class ButlerAIClient implements AIClient {
@ -12,7 +12,7 @@ export class ButlerAIClient implements AIClient {
private modelKind: ModelKind
) {}
async evaluate(prompt: PromptMessage[]) {
async evaluate(prompt: Prompt) {
const response = await this.cloud.post<{ message: string }>('evaluate_prompt/predict.json', {
body: {
messages: prompt,

View File

@ -1,5 +1,5 @@
import { LONG_DEFAULT_BRANCH_TEMPLATE, LONG_DEFAULT_COMMIT_TEMPLATE } from '$lib/ai/prompts';
import { MessageRole, type PromptMessage, type AIClient } from '$lib/ai/types';
import { MessageRole, type PromptMessage, type AIClient, type Prompt } from '$lib/ai/types';
import { isNonEmptyObject } from '$lib/utils/typeguards';
import { fetch, Body, Response } from '@tauri-apps/api/http';
@ -22,7 +22,7 @@ interface OllamaRequestOptions {
interface OllamaChatRequest {
model: string;
messages: PromptMessage[];
messages: Prompt;
stream: boolean;
format?: 'json';
options?: OllamaRequestOptions;
@ -81,7 +81,7 @@ export class OllamaClient implements AIClient {
private modelName: string
) {}
async evaluate(prompt: PromptMessage[]) {
async evaluate(prompt: Prompt) {
const messages = this.formatPrompt(prompt);
const response = await this.chat(messages);
const rawResponse = JSON.parse(response.message.content);
@ -96,7 +96,7 @@ export class OllamaClient implements AIClient {
* Appends a system message which instructs the model to respond using a particular JSON schema
* Modifies the prompt's Assistant messages to make use of the correct schema
*/
private formatPrompt(prompt: PromptMessage[]) {
private formatPrompt(prompt: Prompt) {
const withFormattedResponses = prompt.map((promptMessage) => {
if (promptMessage.role == MessageRole.Assistant) {
return {
@ -146,7 +146,7 @@ ${JSON.stringify(OLLAMA_CHAT_MESSAGE_FORMAT_SCHEMA, null, 2)}`
* @returns A Promise that resolves to an LLMResponse object representing the response from the LLM model.
*/
private async chat(
messages: PromptMessage[],
messages: Prompt,
options?: OllamaRequestOptions
): Promise<OllamaChatResponse> {
const result = await this.fetchChat({

View File

@ -1,5 +1,5 @@
import { SHORT_DEFAULT_BRANCH_TEMPLATE, SHORT_DEFAULT_COMMIT_TEMPLATE } from '$lib/ai/prompts';
import type { OpenAIModelName, PromptMessage, AIClient } from '$lib/ai/types';
import type { OpenAIModelName, Prompt, AIClient } from '$lib/ai/types';
import type OpenAI from 'openai';
export class OpenAIClient implements AIClient {
@ -11,7 +11,7 @@ export class OpenAIClient implements AIClient {
private openAI: OpenAI
) {}
async evaluate(prompt: PromptMessage[]) {
async evaluate(prompt: Prompt) {
const response = await this.openAI.chat.completions.create({
messages: prompt,
model: this.modelName,

View File

@ -0,0 +1,96 @@
import {
LONG_DEFAULT_BRANCH_TEMPLATE,
SHORT_DEFAULT_BRANCH_TEMPLATE,
LONG_DEFAULT_COMMIT_TEMPLATE,
SHORT_DEFAULT_COMMIT_TEMPLATE
} from '$lib/ai/prompts';
import { persisted, type Persisted } from '$lib/persisted/persisted';
import { get } from 'svelte/store';
import type { Prompt, Prompts, UserPrompt } from '$lib/ai/types';
enum PromptPersistedKey {
Branch = 'aiBranchPrompts',
Commit = 'aiCommitPrompts'
}
export class PromptService {
get branchPrompts(): Prompts {
return {
defaultPrompt: LONG_DEFAULT_BRANCH_TEMPLATE,
userPrompts: persisted<UserPrompt[]>([], PromptPersistedKey.Branch)
};
}
get commitPrompts(): Prompts {
return {
defaultPrompt: LONG_DEFAULT_COMMIT_TEMPLATE,
userPrompts: persisted<UserPrompt[]>([], PromptPersistedKey.Commit)
};
}
selectedBranchPromptId(projectId: string): Persisted<string | undefined> {
return persisted<string | undefined>(undefined, `${PromptPersistedKey.Branch}-${projectId}`);
}
selectedBranchPrompt(projectId: string): Prompt | undefined {
const id = get(this.selectedBranchPromptId(projectId));
if (!id) return;
return this.findPrompt(get(this.branchPrompts.userPrompts), id);
}
selectedCommitPromptId(projectId: string): Persisted<string | undefined> {
return persisted<string | undefined>(undefined, `${PromptPersistedKey.Commit}-${projectId}`);
}
selectedCommitPrompt(projectId: string): Prompt | undefined {
const id = get(this.selectedCommitPromptId(projectId));
if (!id) return;
return this.findPrompt(get(this.commitPrompts.userPrompts), id);
}
findPrompt(prompts: UserPrompt[], promptId: string) {
const prompt = prompts.find((userPrompt) => userPrompt.id == promptId)?.prompt;
if (!prompt) return;
if (this.promptMissingContent(prompt)) return;
return prompt;
}
promptEquals(prompt1: Prompt, prompt2: Prompt) {
if (prompt1.length != prompt2.length) return false;
for (const indexPromptMessage of prompt1.entries()) {
const [index, promptMessage] = indexPromptMessage;
if (
promptMessage.role != prompt2[index].role ||
promptMessage.content != prompt2[index].content
) {
return false;
}
}
return true;
}
promptMissingContent(prompt: Prompt) {
for (const promptMessage of prompt) {
if (!promptMessage.content) return true;
}
return false;
}
createDefaultUserPrompt(type: 'commits' | 'branches'): UserPrompt {
return {
id: crypto.randomUUID(),
name: 'My Prompt',
prompt: type == 'branches' ? SHORT_DEFAULT_BRANCH_TEMPLATE : SHORT_DEFAULT_COMMIT_TEMPLATE
};
}
}

View File

@ -1,6 +1,6 @@
import { type PromptMessage, MessageRole } from '$lib/ai/types';
import { type Prompt, MessageRole } from '$lib/ai/types';
export const SHORT_DEFAULT_COMMIT_TEMPLATE: PromptMessage[] = [
export const SHORT_DEFAULT_COMMIT_TEMPLATE: Prompt = [
{
role: MessageRole.User,
content: `Please could you write a commit message for my changes.
@ -16,11 +16,14 @@ Do not start any lines with the hash symbol.
%{emoji_style}
Here is my git diff:
%{diff}`
\`\`\`
%{diff}
\`\`\`
`
}
];
export const LONG_DEFAULT_COMMIT_TEMPLATE: PromptMessage[] = [
export const LONG_DEFAULT_COMMIT_TEMPLATE: Prompt = [
{
role: MessageRole.User,
content: `Please could you write a commit message for my changes.
@ -34,6 +37,7 @@ Do not start any lines with the hash symbol.
Only respond with the commit message.
Here is my git diff:
\`\`\`
diff --git a/src/utils/typing.ts b/src/utils/typing.ts
index 1cbfaa2..7aeebcf 100644
--- a/src/utils/typing.ts
@ -48,7 +52,9 @@ index 1cbfaa2..7aeebcf 100644
+ check: (value: unknown) => value is T
+): something is T[] {
+ return Array.isArray(something) && something.every(check);
+}`
+}
\`\`\`
`
},
{
role: MessageRole.Assistant,
@ -59,7 +65,7 @@ Added an utility function to check whether a given value is an array of a specif
...SHORT_DEFAULT_COMMIT_TEMPLATE
];
export const SHORT_DEFAULT_BRANCH_TEMPLATE: PromptMessage[] = [
export const SHORT_DEFAULT_BRANCH_TEMPLATE: Prompt = [
{
role: MessageRole.User,
content: `Please could you write a branch name for my changes.
@ -69,11 +75,14 @@ Branch names should contain a maximum of 5 words.
Only respond with the branch name.
Here is my git diff:
%{diff}`
\`\`\`
%{diff}
\`\`\`
`
}
];
export const LONG_DEFAULT_BRANCH_TEMPLATE: PromptMessage[] = [
export const LONG_DEFAULT_BRANCH_TEMPLATE: Prompt = [
{
role: MessageRole.User,
content: `Please could you write a branch name for my changes.
@ -83,6 +92,7 @@ Branch names should contain a maximum of 5 words.
Only respond with the branch name.
Here is my git diff:
\`\`\`
diff --git a/src/utils/typing.ts b/src/utils/typing.ts
index 1cbfaa2..7aeebcf 100644
--- a/src/utils/typing.ts
@ -97,7 +107,9 @@ index 1cbfaa2..7aeebcf 100644
+ check: (value: unknown) => value is T
+): something is T[] {
+ return Array.isArray(something) && something.every(check);
+}`
+}
\`\`\`
`
},
{
role: MessageRole.Assistant,

View File

@ -8,7 +8,7 @@ import {
ModelKind,
OpenAIModelName,
type AIClient,
type PromptMessage
type Prompt
} from '$lib/ai/types';
import { HttpClient } from '$lib/backend/httpClient';
import * as toasts from '$lib/utils/toasts';
@ -51,7 +51,7 @@ class DummyAIClient implements AIClient {
defaultBranchTemplate = SHORT_DEFAULT_BRANCH_TEMPLATE;
constructor(private response = 'lorem ipsum') {}
async evaluate(_prompt: PromptMessage[]) {
async evaluate(_prompt: Prompt) {
return this.response;
}
}

View File

@ -11,8 +11,8 @@ import {
type AIClient,
AnthropicModelName,
ModelKind,
type PromptMessage,
MessageRole
MessageRole,
type Prompt
} from '$lib/ai/types';
import { splitMessage } from '$lib/utils/commitMessage';
import * as toasts from '$lib/utils/toasts';
@ -45,13 +45,13 @@ type SummarizeCommitOpts = {
hunks: Hunk[];
useEmojiStyle?: boolean;
useBriefStyle?: boolean;
commitTemplate?: PromptMessage[];
commitTemplate?: Prompt;
userToken?: string;
};
type SummarizeBranchOpts = {
hunks: Hunk[];
branchTemplate?: PromptMessage[];
branchTemplate?: Prompt;
userToken?: string;
};

View File

@ -1,3 +1,5 @@
import type { Persisted } from '$lib/persisted/persisted';
export enum ModelKind {
OpenAI = 'openai',
Anthropic = 'anthropic',
@ -28,9 +30,22 @@ export interface PromptMessage {
role: MessageRole;
}
export interface AIClient {
evaluate(prompt: PromptMessage[]): Promise<string>;
export type Prompt = PromptMessage[];
defaultBranchTemplate: PromptMessage[];
defaultCommitTemplate: PromptMessage[];
export interface AIClient {
evaluate(prompt: Prompt): Promise<string>;
defaultBranchTemplate: Prompt;
defaultCommitTemplate: Prompt;
}
export interface UserPrompt {
id: string;
name: string;
prompt: Prompt;
}
export interface Prompts {
defaultPrompt: Prompt;
userPrompts: Persisted<UserPrompt[]>;
}

View File

@ -0,0 +1,79 @@
<script lang="ts">
import { PromptService } from '$lib/ai/promptService';
import Content from '$lib/components/AIPromptEdit/Content.svelte';
import Button from '$lib/components/Button.svelte';
import { getContext } from '$lib/utils/context';
import { get } from 'svelte/store';
import type { Prompts, UserPrompt } from '$lib/ai/types';
export let promptUse: 'commits' | 'branches';
const promptService = getContext(PromptService);
let prompts: Prompts;
if (promptUse == 'commits') {
prompts = promptService.commitPrompts;
} else {
prompts = promptService.branchPrompts;
}
$: userPrompts = prompts.userPrompts;
function createNewPrompt() {
prompts.userPrompts.set([
...get(prompts.userPrompts),
promptService.createDefaultUserPrompt(promptUse)
]);
}
function deletePrompt(targetPrompt: UserPrompt) {
const filteredPrompts = get(prompts.userPrompts).filter((prompt) => prompt != targetPrompt);
prompts.userPrompts.set(filteredPrompts);
}
</script>
{#if prompts && $userPrompts}
<div class="prompt-item__title">
<h3 class="text-base-15 text-bold">
{promptUse == 'commits' ? 'Commit Message' : 'Branch Name'}
</h3>
<Button kind="solid" style="ghost" icon="plus-small" on:click={createNewPrompt}
>New prompt</Button
>
</div>
<div class="content">
<Content
displayMode="readOnly"
prompt={{
prompt: prompts.defaultPrompt,
name: 'Default Prompt',
id: 'default'
}}
/>
{#each $userPrompts as prompt}
<Content
bind:prompt
displayMode="writable"
on:deletePrompt={(e) => deletePrompt(e.detail.prompt)}
/>
{/each}
</div>
{/if}
<style lang="postcss">
.prompt-item__title {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--size-24);
}
.content {
display: flex;
flex-direction: column;
gap: var(--size-6);
}
</style>

View File

@ -0,0 +1,227 @@
<script lang="ts">
import { MessageRole, type UserPrompt } from '$lib/ai/types';
import DialogBubble from '$lib/components/AIPromptEdit/DialogBubble.svelte';
import Button from '$lib/components/Button.svelte';
import Icon from '$lib/components/Icon.svelte';
import TextBox from '$lib/components/TextBox.svelte';
import { createEventDispatcher } from 'svelte';
export let displayMode: 'readOnly' | 'writable' = 'writable';
export let prompt: UserPrompt;
let expanded = false;
let editing = false;
let promptMessages = structuredClone(prompt.prompt);
let promptName = structuredClone(prompt.name);
let initialName = promptName;
// Ensure the prompt messages have a default user prompt
if (promptMessages.length == 0) {
promptMessages = [
...promptMessages,
{
role: MessageRole.User,
content: ''
}
];
}
function addExample() {
promptMessages = [
...promptMessages,
{
role: MessageRole.Assistant,
content: ''
},
{
role: MessageRole.User,
content: ''
}
];
}
function removeLastExample() {
console.log(promptMessages);
promptMessages = promptMessages.slice(0, -2);
}
const dispatcher = createEventDispatcher<{ deletePrompt: { prompt: UserPrompt } }>();
function deletePrompt() {
dispatcher('deletePrompt', { prompt });
}
let errorMessages = [] as number[];
function save() {
errorMessages = checkForEmptyMessages();
if (errorMessages.length > 0) {
return;
}
if (promptName.trim() == '') {
promptName = initialName;
}
prompt.prompt = promptMessages;
prompt.name = promptName;
editing = false;
}
function cancel() {
promptMessages = structuredClone(prompt.prompt);
promptName = structuredClone(prompt.name);
editing = false;
}
$: isInEditing = displayMode == 'writable' && editing;
function toggleExpand() {
if (isInEditing) return;
expanded = !expanded;
}
function checkForEmptyMessages() {
let errors = [] as number[];
promptMessages.forEach((message, index) => {
if (message.content.trim() == '') {
errors.push(index);
}
});
return errors;
}
</script>
<div class="card">
<div
tabindex="0"
role="button"
class="header"
class:editing={isInEditing}
on:click={toggleExpand}
on:keydown={(e) => e.key === 'Enter' && toggleExpand()}
>
{#if !isInEditing}
<Icon name="doc" />
<h3 class="text-base-15 text-bold title">{promptName}</h3>
<div class="icon">
<Icon name={expanded ? 'chevron-up' : 'chevron-down'} />
</div>
{:else}
<TextBox bind:value={promptName} wide on:click={(e) => e.stopPropagation()} />
{/if}
</div>
{#if expanded}
<div class="content" class:default-mode={prompt.id == 'default'} class:editing={isInEditing}>
{#each promptMessages as promptMessage, index}
<DialogBubble
bind:promptMessage
editing={isInEditing}
isLast={index + 1 == promptMessages.length || promptMessages.length == 1}
disableRemove={promptMessages.length == 1}
on:addExample={addExample}
on:removeLastExample={removeLastExample}
on:input={() => {
errorMessages = errorMessages.filter((errorIndex) => errorIndex != index);
}}
isError={errorMessages.includes(index)}
/>
{#if index % 2 == 0}
<hr class="sections-divider" />
{/if}
{/each}
</div>
{#if displayMode == 'writable'}
<div class="actions">
{#if editing}
<Button kind="solid" style="ghost" on:click={() => cancel()}>Cancel</Button>
<Button
disabled={errorMessages.length > 0}
kind="solid"
style="pop"
on:click={() => save()}>Save Changes</Button
>
{:else}
<Button
style="error"
on:click={(e) => {
e.stopPropagation();
deletePrompt();
}}
icon="bin-small">Delete</Button
>
<Button kind="solid" style="ghost" icon="edit-text" on:click={() => (editing = true)}
>Edit Prompt</Button
>
{/if}
</div>
{/if}
{/if}
</div>
<style lang="postcss">
.header {
cursor: pointer;
display: flex;
align-items: center;
gap: var(--size-16);
padding: var(--size-16);
&.editing {
cursor: default;
}
& .title {
flex: 1;
}
&.editing {
cursor: default;
}
& .icon {
color: var(--clr-text-2);
}
}
.content {
display: flex;
flex-direction: column;
gap: var(--size-16);
padding: var(--size-16) 0;
border-top: 1px solid var(--clr-border-3);
}
.sections-divider {
user-select: none;
border-top: 1px solid var(--clr-border-3);
&.empty {
opacity: 0;
}
}
.actions {
display: flex;
justify-content: flex-end;
gap: var(--size-8);
padding: 0 var(--size-16) var(--size-16);
}
.default-mode {
padding: var(--size-16) 0;
border-top: 1px solid var(--clr-border-3);
& .sections-divider {
display: none;
}
}
</style>

View File

@ -0,0 +1,216 @@
<script lang="ts">
import { MessageRole } from '$lib/ai/types';
import Button from '$lib/components/Button.svelte';
import Icon from '$lib/components/Icon.svelte';
import { useAutoHeight } from '$lib/utils/useAutoHeight';
import { useResize } from '$lib/utils/useResize';
import { marked } from 'marked';
import { createEventDispatcher } from 'svelte';
export let disableRemove = false;
export let isError = false;
export let isLast = false;
export let autofocus = false;
export let editing = false;
export let promptMessage: { role: MessageRole; content: string };
const dispatcher = createEventDispatcher<{
removeLastExample: void;
addExample: void;
input: string;
}>();
let textareaElement: HTMLTextAreaElement | undefined;
function focusTextareaOnMount(
textareaElement: HTMLTextAreaElement | undefined,
autofocus: boolean,
editing: boolean
) {
if (textareaElement && autofocus && editing) {
textareaElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
textareaElement.focus();
}
}
$: focusTextareaOnMount(textareaElement, autofocus, editing);
$: if (textareaElement) useAutoHeight(textareaElement);
</script>
<div
class="bubble-wrap"
class:editing
class:bubble-wrap_user={promptMessage.role == MessageRole.User}
class:bubble-wrap_assistant={promptMessage.role == MessageRole.Assistant}
>
<div class="bubble">
<div class="bubble__header text-base-13 text-bold">
{#if promptMessage.role == MessageRole.User}
<Icon name="profile" />
<span>User</span>
{:else}
<Icon name="robot" />
<span>Assistant</span>
{/if}
</div>
{#if editing}
<textarea
bind:this={textareaElement}
bind:value={promptMessage.content}
class="textarea scrollbar text-base-body-13"
class:is-error={isError}
rows={1}
on:input={(e) => {
useAutoHeight(e.currentTarget);
dispatcher('input', e.currentTarget.value);
}}
on:change={(e) => {
useAutoHeight(e.currentTarget);
}}
use:useResize={() => {
if (textareaElement) useAutoHeight(textareaElement);
}}
/>
{:else}
<div class="markdown bubble-message scrollbar text-base-body-13">
{@html marked.parse(promptMessage.content)}
</div>
{/if}
</div>
{#if isLast && editing}
<div class="bubble-actions">
{#if !disableRemove}
<Button
icon="bin-small"
kind="soft"
style="error"
on:click={() => dispatcher('removeLastExample')}
>
Remove example
</Button>
{/if}
<Button kind="solid" style="ghost" grow on:click={() => dispatcher('addExample')}
>Add new example</Button
>
</div>
{/if}
</div>
<style lang="post-css">
.bubble-wrap {
display: flex;
flex-direction: column;
width: 100%;
padding: 0 var(--size-16);
&.editing {
& .bubble__header {
border: 1px solid var(--clr-border-2);
border-bottom: none;
}
}
}
.bubble {
width: 100%;
max-width: 90%;
/* overflow: hidden; */
}
.bubble-wrap_user {
align-items: flex-end;
& .bubble__header,
& .bubble-message {
background-color: var(--clr-bg-2);
}
}
.bubble-wrap_assistant {
align-items: flex-start;
& .bubble__header,
& .bubble-message {
background-color: var(--clr-theme-pop-bg);
}
}
.bubble__header {
display: flex;
align-items: center;
gap: var(--size-8);
padding: var(--size-12);
/* border: 1px solid var(--clr-border-2); */
border-bottom: none;
border-radius: var(--radius-l) var(--radius-l) 0 0;
}
.bubble-message {
overflow-x: auto;
color: var(--clr-text-1);
border-top: 1px solid var(--clr-border-2);
/* border: 1px solid var(--clr-border-2); */
border-radius: 0 0 var(--radius-l) var(--radius-l);
padding: var(--size-12);
}
.bubble-actions {
display: flex;
width: 90%;
margin-top: var(--size-12);
margin-bottom: var(--size-8);
gap: var(--size-8);
}
.textarea {
width: 100%;
resize: none;
background: none;
border: none;
outline: none;
padding: var(--size-12);
background-color: var(--clr-bg-1);
border: 1px solid var(--clr-border-2);
border-radius: 0 0 var(--radius-l) var(--radius-l);
transition:
background-color var(--transition-fast),
border-color var(--transition-fast);
&:not(.is-error):hover,
&:not(.is-error):focus-within {
border-color: var(--clr-border-1);
}
}
/* MODIFIERS */
.is-error {
animation: shake 0.25s ease-in-out forwards;
}
@keyframes shake {
0% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
50% {
transform: translateX(5px);
border: 1px solid var(--clr-theme-err-element);
}
75% {
transform: translateX(-5px);
border: 1px solid var(--clr-theme-err-element);
}
100% {
transform: translateX(0);
border: 1px solid var(--clr-theme-err-element);
}
}
</style>

View File

@ -0,0 +1,58 @@
<script lang="ts">
import Select from './Select.svelte';
import { PromptService } from '$lib/ai/promptService';
import { Project } from '$lib/backend/projects';
import SelectItem from '$lib/components/SelectItem.svelte';
import { getContext } from '$lib/utils/context';
import type { Prompts, UserPrompt } from '$lib/ai/types';
import type { Persisted } from '$lib/persisted/persisted';
export let promptUse: 'commits' | 'branches';
const project = getContext(Project);
const promptService = getContext(PromptService);
let prompts: Prompts;
let selectedPromptId: Persisted<string | undefined>;
if (promptUse == 'commits') {
prompts = promptService.commitPrompts;
selectedPromptId = promptService.selectedCommitPromptId(project.id);
} else {
prompts = promptService.branchPrompts;
selectedPromptId = promptService.selectedBranchPromptId(project.id);
}
let userPrompts = prompts.userPrompts;
let allPrompts: UserPrompt[] = [];
const defaultId = crypto.randomUUID();
function setAllPrompts(userPrompts: UserPrompt[]) {
allPrompts = [
{ name: 'Default Prompt', id: defaultId, prompt: prompts.defaultPrompt },
...userPrompts
];
}
$: setAllPrompts($userPrompts);
$: if (!$selectedPromptId || !promptService.findPrompt(allPrompts, $selectedPromptId)) {
$selectedPromptId = defaultId;
}
</script>
<Select
items={allPrompts}
bind:selectedItemId={$selectedPromptId}
itemId="id"
labelId="name"
disabled={allPrompts.length == 1}
wide={true}
label={promptUse == 'commits' ? 'Commit Message' : 'Branch Name'}
>
<SelectItem slot="template" let:item let:selected {selected}>
{item.name}
</SelectItem>
</Select>

View File

@ -9,6 +9,7 @@
import InfoMessage from './InfoMessage.svelte';
import PullRequestCard from './PullRequestCard.svelte';
import ScrollableContainer from './ScrollableContainer.svelte';
import { PromptService } from '$lib/ai/promptService';
import { AIService } from '$lib/ai/service';
import laneNewSvg from '$lib/assets/empty-state/lane-new.svg?raw';
import noChangesSvg from '$lib/assets/empty-state/lane-no-changes.svg?raw';
@ -54,6 +55,7 @@
const aiGenAutoBranchNamingEnabled = projectAiGenAutoBranchNamingEnabled(project.id);
const aiService = getContext(AIService);
const promptService = getContext(PromptService);
const userSettings = getContextStoreBySymbol<Settings>(SETTINGS);
const defaultBranchWidthRem = persisted<number>(24, 'defaulBranchWidth' + project.id);
@ -75,9 +77,11 @@
const hunks = branch.files.flatMap((f) => f.hunks);
try {
const prompt = promptService.selectedBranchPrompt(project.id);
const message = await aiService.summarizeBranch({
hunks,
userToken: $user?.access_token
userToken: $user?.access_token,
branchTemplate: prompt
});
if (message && message !== branch.name) {

View File

@ -2,6 +2,8 @@
import SectionCard from './SectionCard.svelte';
import WelcomeSigninAction from './WelcomeSigninAction.svelte';
import { Project, ProjectService } from '$lib/backend/projects';
import AiPromptSelect from '$lib/components/AIPromptSelect.svelte';
import Button from '$lib/components/Button.svelte';
import Link from '$lib/components/Link.svelte';
import Spacer from '$lib/components/Spacer.svelte';
import Toggle from '$lib/components/Toggle.svelte';
@ -12,6 +14,7 @@
import { getContext } from '$lib/utils/context';
import * as toasts from '$lib/utils/toasts';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { PUBLIC_API_BASE_URL } from '$env/static/public';
const userService = getContext(UserService);
@ -104,6 +107,26 @@
</svelte:fragment>
</SectionCard>
</div>
<SectionCard>
<svelte:fragment slot="title">Custom prompts</svelte:fragment>
<AiPromptSelect promptUse="commits" />
<AiPromptSelect promptUse="branches" />
<Spacer margin={8} />
<p class="text-base-body-12">
You can apply your own custom prompts to the project. By default, the project uses GitButler
prompts, but you can create your own prompts in the general settings.
</p>
<Button
style="ghost"
kind="solid"
icon="edit-text"
on:click={async () => await goto('/settings/ai')}>Customize prompts</Button
>
</SectionCard>
</Section>
{#if $user?.role === 'admin'}

View File

@ -1,4 +1,5 @@
<script lang="ts">
import { PromptService } from '$lib/ai/promptService';
import { AIService } from '$lib/ai/service';
import { Project } from '$lib/backend/projects';
import Checkbox from '$lib/components/Checkbox.svelte';
@ -33,6 +34,7 @@
const aiService = getContext(AIService);
const branch = getContextStore(Branch);
const project = getContext(Project);
const promptService = getContext(PromptService);
const dispatch = createEventDispatcher<{
action: 'generate-branch-name';
@ -81,11 +83,14 @@
aiLoading = true;
try {
const prompt = promptService.selectedCommitPrompt(project.id);
console.log(prompt);
const generatedMessage = await aiService.summarizeCommit({
hunks,
useEmojiStyle: $commitGenerationUseEmojis,
useBriefStyle: $commitGenerationExtraConcise,
userToken: $user?.access_token
userToken: $user?.access_token,
commitTemplate: prompt
});
if (generatedMessage) {

View File

@ -52,6 +52,7 @@
project.description = description;
saveProject();
}}
maxHeight={300}
/>
</section>
</fieldset>

View File

@ -48,7 +48,7 @@
{style}
{kind}
{help}
icon={visible ? 'chevron-top' : 'chevron-down'}
icon={visible ? 'chevron-up' : 'chevron-down'}
{loading}
disabled={disabled || loading}
isDropdownChild

View File

@ -0,0 +1,33 @@
<script lang="ts">
import SectionCard from '$lib/components/SectionCard.svelte';
export let roundedTop = true;
export let roundedBottom = true;
export let expanded = false;
function maybeToggle() {
expanded = !expanded;
}
</script>
<SectionCard
{roundedTop}
roundedBottom={roundedBottom && !expanded}
bottomBorder={!expanded}
clickable
on:click={maybeToggle}
>
<svelte:fragment slot="title">
<slot name="header" {expanded}></slot>
</svelte:fragment>
</SectionCard>
{#if expanded}
<SectionCard hasTopRadius={false} roundedTop={false} {roundedBottom} topDivider>
<slot></slot>
</SectionCard>
<SectionCard hasTopRadius={false} roundedTop={false} {roundedBottom} topDivider>
<slot name="actions"></slot>
</SectionCard>
{/if}

View File

@ -87,7 +87,7 @@
position: absolute;
width: var(--size-2);
background-color: var(--clr-commit-remote);
left: calc(var(--size-10) + var(--size-1));
left: calc(var(--size-10) + 0.063rem);
bottom: 0;
top: 0;
&.first {
@ -138,7 +138,7 @@
top: 1.875rem;
border-radius: var(--radius-l) 0 0 0;
height: var(--size-16);
left: calc(var(--size-10) + var(--size-1));
left: calc(var(--size-10) + 0.063rem);
border-color: var(--clr-commit-local);
border-width: var(--size-2) 0 0 var(--size-2);
&.base {

View File

@ -16,12 +16,15 @@
export let noBorder = false;
export let labelFor = '';
export let disabled = false;
export let clickable = false;
const SLOTS = $$props.$$slots;
const dispatch = createEventDispatcher<{ hover: boolean }>();
</script>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<label
for={labelFor}
class="section-card"
@ -36,8 +39,9 @@
class:loading={background == 'loading'}
class:success={background == 'success'}
class:error={background == 'error'}
class:clickable={labelFor !== ''}
class:clickable={labelFor !== '' || clickable}
class:disabled
on:click
on:mouseenter={() => dispatch('hover', true)}
on:mouseleave={() => dispatch('hover', false)}
>
@ -128,6 +132,11 @@
display: flex;
}
.section-card__icon-side {
display: flex;
align-items: center;
}
/* MODIFIERS */
.rounded-top {
@ -150,8 +159,7 @@
display: block;
width: 100%;
height: 1px;
background-color: var(--clr-border-2);
opacity: 0.5;
background-color: var(--clr-border-3);
}
}

View File

@ -187,6 +187,7 @@
spellcheck
id="comments"
rows={6}
maxHeight={400}
bind:value={messageInputValue}
/>

View File

@ -1,22 +1,27 @@
<script lang="ts">
import { pxToRem } from '$lib/utils/pxToRem';
export let margin: number | undefined = 16;
export let noLine = false;
function getMargins() {
if (margin === undefined) {
return '';
}
return `margin-top: var(--size-${margin}); margin-bottom: var(--size-${margin});`;
return `margin-top: ${pxToRem(margin)}; margin-bottom: ${pxToRem(margin)};`;
}
</script>
<div class="divider" style={getMargins()} />
<div class="divider" style={getMargins()} class:line={!noLine}></div>
<style lang="post-css">
.divider {
height: 1px;
width: 100%;
border-bottom: 1px solid var(--clr-scale-ntrl-0);
opacity: 0.15;
}
.divider.line {
border-bottom: 1px solid var(--clr-scale-ntrl-0);
}
</style>

View File

@ -234,6 +234,7 @@
.shrinkable {
overflow: hidden;
width: fit-content;
& span {
overflow: hidden;

View File

@ -1,10 +1,14 @@
<script lang="ts">
import { pxToRem } from '$lib/utils/pxToRem';
import { useAutoHeight } from '$lib/utils/useAutoHeight';
import { useResize } from '$lib/utils/useResize';
import { createEventDispatcher } from 'svelte';
export let value: string | undefined;
export let placeholder: string | undefined = undefined;
export let required = false;
export let rows = 4;
export let maxHeight: number | undefined = undefined;
export let id: string | undefined = undefined;
export let disabled = false;
export let autocomplete: string | undefined = undefined;
@ -13,6 +17,8 @@
export let label: string | undefined = undefined;
const dispatch = createEventDispatcher<{ input: string; change: string }>();
let textareaElement: HTMLTextAreaElement;
</script>
<div class="textarea-wrapper">
@ -22,7 +28,8 @@
</label>
{/if}
<textarea
class="text-input text-base-body-13 textarea"
bind:this={textareaElement}
class="text-input text-base-body-13 textarea scrollbar"
bind:value
{disabled}
{id}
@ -33,8 +40,19 @@
{autocomplete}
{autocorrect}
{spellcheck}
on:input={(e) => dispatch('input', e.currentTarget.value)}
on:change={(e) => dispatch('change', e.currentTarget.value)}
on:input={(e) => {
dispatch('input', e.currentTarget.value);
useAutoHeight(e.currentTarget);
}}
on:change={(e) => {
dispatch('change', e.currentTarget.value);
useAutoHeight(e.currentTarget);
}}
use:useResize={() => {
useAutoHeight(textareaElement);
}}
on:focus={(e) => useAutoHeight(e.currentTarget)}
style:max-height={maxHeight ? pxToRem(maxHeight) : undefined}
/>
</div>
@ -48,8 +66,11 @@
.textarea {
width: 100%;
resize: none;
padding-top: var(--size-12);
padding-bottom: var(--size-12);
padding: var(--size-12);
&::-webkit-resizer {
background: transparent;
}
}
.textbox__label {

View File

@ -12,7 +12,7 @@
"chevron-left-small": "M7.14018 8.27406C7.04255 8.17643 7.04255 8.01814 7.14018 7.92051L10.4331 4.62762L9.37241 3.56696L6.07952 6.85985C5.3961 7.54327 5.3961 8.65131 6.07952 9.33472L9.37241 12.6276L10.4331 11.567L7.14018 8.27406Z",
"chevron-right": "M9.76256 8.17678C9.86019 8.07914 9.86019 7.92085 9.76256 7.82322L5.46967 3.53033L6.53033 2.46967L10.8232 6.76256C11.5066 7.44598 11.5066 8.55402 10.8232 9.23744L6.53033 13.5303L5.46967 12.4697L9.76256 8.17678Z",
"chevron-right-small": "M8.85983 7.92049C8.95747 8.01812 8.95747 8.17642 8.85983 8.27405L5.56694 11.5669L6.6276 12.6276L9.92049 9.33471C10.6039 8.65129 10.6039 7.54325 9.92049 6.85983L6.6276 3.56694L5.56694 4.6276L8.85983 7.92049Z",
"chevron-top": "M7.82322 6.23744C7.92086 6.13981 8.07915 6.13981 8.17678 6.23744L12.4697 10.5303L13.5303 9.46967L9.23744 5.17678C8.55402 4.49336 7.44598 4.49336 6.76256 5.17678L2.46967 9.46967L3.53033 10.5303L7.82322 6.23744Z",
"chevron-up": "M7.82322 6.23744C7.92086 6.13981 8.07915 6.13981 8.17678 6.23744L12.4697 10.5303L13.5303 9.46967L9.23744 5.17678C8.55402 4.49336 7.44598 4.49336 6.76256 5.17678L2.46967 9.46967L3.53033 10.5303L7.82322 6.23744Z",
"chevron-up-small": "M7.82323 7.23744C7.92086 7.13981 8.07915 7.13981 8.17678 7.23744L11.4697 10.5303L12.5303 9.46967L9.23744 6.17678C8.55402 5.49336 7.44598 5.49336 6.76257 6.17678L3.46967 9.46967L4.53033 10.5303L7.82323 7.23744Z",
"closed-pr-small": "M8.93934 6L6.46967 3.53033L7.53033 2.46967L10 4.93934L12.4697 2.46967L13.5303 3.53033L11.0607 6L13.5303 8.46967L12.4697 9.53033L10 7.06066L7.53033 9.53033L6.46967 8.46967L8.93934 6Z M3.25 4V13H4.75V4H3.25Z M9.25 9V13H10.75V9H9.25Z",
"commit": "M4.32501 7.25C4.67247 5.53832 6.18578 4.25 8 4.25C9.81422 4.25 11.3275 5.53832 11.675 7.25H15V8.75H11.675C11.3275 10.4617 9.81422 11.75 8 11.75C6.18578 11.75 4.67247 10.4617 4.32501 8.75H1V7.25H4.32501ZM8 5.75C6.75736 5.75 5.75 6.75736 5.75 8C5.75 9.24264 6.75736 10.25 8 10.25C9.24264 10.25 10.25 9.24264 10.25 8C10.25 6.75736 9.24264 5.75 8 5.75Z",
@ -67,7 +67,7 @@
"pr-closed": "M4.5 0.75C2.98122 0.75 1.75 1.98122 1.75 3.5C1.75 4.75878 2.59575 5.82002 3.75 6.14648V9.85352C2.59575 10.18 1.75 11.2412 1.75 12.5C1.75 14.0188 2.98122 15.25 4.5 15.25C6.01878 15.25 7.25 14.0188 7.25 12.5C7.25 11.2412 6.40425 10.18 5.25 9.85352V6.14648C6.40425 5.82002 7.25 4.75878 7.25 3.5C7.25 1.98122 6.01878 0.75 4.5 0.75ZM3.25 3.5C3.25 2.80964 3.80964 2.25 4.5 2.25C5.19036 2.25 5.75 2.80964 5.75 3.5C5.75 4.19036 5.19036 4.75 4.5 4.75C3.80964 4.75 3.25 4.19036 3.25 3.5ZM3.25 12.5C3.25 11.8096 3.80964 11.25 4.5 11.25C5.19036 11.25 5.75 11.8096 5.75 12.5C5.75 13.1904 5.19036 13.75 4.5 13.75C3.80964 13.75 3.25 13.1904 3.25 12.5Z M12.25 9.85352V7H10.75V9.85352C9.59575 10.18 8.75 11.2412 8.75 12.5C8.75 14.0188 9.98122 15.25 11.5 15.25C13.0188 15.25 14.25 14.0188 14.25 12.5C14.25 11.2412 13.4043 10.18 12.25 9.85352ZM10.25 12.5C10.25 11.8096 10.8096 11.25 11.5 11.25C12.1904 11.25 12.75 11.8096 12.75 12.5C12.75 13.1904 12.1904 13.75 11.5 13.75C10.8096 13.75 10.25 13.1904 10.25 12.5Z M10.4393 3.75L8.71967 2.03033L9.78033 0.96967L11.5 2.68934L13.2197 0.96967L14.2803 2.03033L12.5607 3.75L14.2803 5.46967L13.2197 6.53033L11.5 4.81066L9.78033 6.53033L8.71967 5.46967L10.4393 3.75Z",
"pr-draft": "M4.5 0.75C2.98122 0.75 1.75 1.98122 1.75 3.5C1.75 4.75878 2.59575 5.82002 3.75 6.14648V9.85352C2.59575 10.18 1.75 11.2412 1.75 12.5C1.75 14.0188 2.98122 15.25 4.5 15.25C6.01878 15.25 7.25 14.0188 7.25 12.5C7.25 11.2412 6.40425 10.18 5.25 9.85352V6.14648C6.40425 5.82002 7.25 4.75878 7.25 3.5C7.25 1.98122 6.01878 0.75 4.5 0.75ZM3.25 3.5C3.25 2.80964 3.80964 2.25 4.5 2.25C5.19036 2.25 5.75 2.80964 5.75 3.5C5.75 4.19036 5.19036 4.75 4.5 4.75C3.80964 4.75 3.25 4.19036 3.25 3.5ZM3.25 12.5C3.25 11.8096 3.80964 11.25 4.5 11.25C5.19036 11.25 5.75 11.8096 5.75 12.5C5.75 13.1904 5.19036 13.75 4.5 13.75C3.80964 13.75 3.25 13.1904 3.25 12.5Z M10.75 2V3.5H12.25V2H10.75Z M10.75 7V5H12.25V7H10.75Z M10.75 8.5V9.85352C9.59575 10.18 8.75 11.2412 8.75 12.5C8.75 14.0188 9.98122 15.25 11.5 15.25C13.0188 15.25 14.25 14.0188 14.25 12.5C14.25 11.2412 13.4043 10.18 12.25 9.85352V8.5H10.75ZM11.5 11.25C10.8096 11.25 10.25 11.8096 10.25 12.5C10.25 13.1904 10.8096 13.75 11.5 13.75C12.1904 13.75 12.75 13.1904 12.75 12.5C12.75 11.8096 12.1904 11.25 11.5 11.25Z",
"pr-small": "M3 6L8 3V5.25H10C11.5188 5.25 12.75 6.48122 12.75 8V13H11.25V8C11.25 7.30964 10.6904 6.75 10 6.75H8V9L3 6Z M4.25 9L4.25 13H5.75L5.75 9H4.25Z",
"profile": "M4.25 5C4.25 2.92893 5.92893 1.25 8 1.25C10.0711 1.25 11.75 2.92893 11.75 5V6C11.75 8.07107 10.0711 9.75 8 9.75C5.92893 9.75 4.25 8.07107 4.25 6V5ZM8 2.75C6.75736 2.75 5.75 3.75736 5.75 5V6C5.75 7.24264 6.75736 8.25 8 8.25C9.24264 8.25 10.25 7.24264 10.25 6V5C10.25 3.75736 9.24264 2.75 8 2.75Z M2.25 14C2.25 11.9289 3.92893 10.25 6 10.25H10C12.0711 10.25 13.75 11.9289 13.75 14H12.25C12.25 12.7574 11.2426 11.75 10 11.75H6C4.75736 11.75 3.75 12.7574 3.75 14H2.25Z",
"profile": "M4.25 5.5C4.25 3.42893 5.92893 1.75 8 1.75C10.0711 1.75 11.75 3.42893 11.75 5.5V6C11.75 8.07107 10.0711 9.75 8 9.75C5.92893 9.75 4.25 8.07107 4.25 6V5.5ZM8 3.25C6.75736 3.25 5.75 4.25736 5.75 5.5V6C5.75 7.24264 6.75736 8.25 8 8.25C9.24264 8.25 10.25 7.24264 10.25 6V5.5C10.25 4.25736 9.24264 3.25 8 3.25Z M2.25 14C2.25 11.9289 3.92893 10.25 6 10.25H10C12.0711 10.25 13.75 11.9289 13.75 14H12.25C12.25 12.7574 11.2426 11.75 10 11.75H6C4.75736 11.75 3.75 12.7574 3.75 14H2.25Z",
"pull-small": "M8.74996 9.91806L12.0017 7.02758L12.9983 8.14869L9.16266 11.5581C8.49961 12.1475 7.50043 12.1475 6.83738 11.5581L3.00175 8.14869L3.99829 7.02758L7.24996 9.91795V4.58795H8.74996V9.91806Z",
"push-small": "M7.2501 6.08194L3.99832 8.97242L3.00177 7.8513L6.83741 4.44185C7.50046 3.85247 8.49963 3.85247 9.16268 4.44185L12.9983 7.8513L12.0018 8.97242L8.7501 6.08205V11.412H7.2501V6.08194Z",
"question-mark": "M8 1.75C4.54822 1.75 1.75 4.54822 1.75 8C1.75 11.4518 4.54822 14.25 8 14.25C11.4518 14.25 14.25 11.4518 14.25 8C14.25 4.54822 11.4518 1.75 8 1.75ZM0.25 8C0.25 3.71979 3.71979 0.25 8 0.25C12.2802 0.25 15.75 3.71979 15.75 8C15.75 12.2802 12.2802 15.75 8 15.75C3.71979 15.75 0.25 12.2802 0.25 8Z M8 4.75C7.30964 4.75 6.75 5.30964 6.75 6V7H5.25V6C5.25 4.48122 6.48122 3.25 8 3.25C9.51878 3.25 10.75 4.48122 10.75 6V6.12132C10.75 6.88284 10.4475 7.61317 9.90901 8.15165L8.53033 9.53033L7.46967 8.46967L8.84835 7.09099C9.10552 6.83382 9.25 6.48502 9.25 6.12132V6C9.25 5.30964 8.69036 4.75 8 4.75Z M9 11C9 11.5523 8.55229 12 8 12C7.44772 12 7 11.5523 7 11C7 10.4477 7.44772 10 8 10C8.55229 10 9 10.4477 9 11Z",
@ -127,5 +127,7 @@
"item-dot": "M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z M2 5C2 3.34315 3.34315 2 5 2H11C12.6569 2 14 3.34315 14 5V11C14 12.6569 12.6569 14 11 14H5C3.34315 14 2 12.6569 2 11V5ZM11 12.5H5C4.17157 12.5 3.5 11.8284 3.5 11V5C3.5 4.17157 4.17157 3.5 5 3.5H11C11.8284 3.5 12.5 4.17157 12.5 5V11C12.5 11.8284 11.8284 12.5 11 12.5Z",
"item-cross": "M5.90535 11.1553L8 9.06063L10.0947 11.1553L11.1553 10.0946L9.06066 7.99996L11.1553 5.90531L10.0947 4.84465L8 6.9393L5.90535 4.84465L4.84469 5.90531L6.93934 7.99996L4.84469 10.0946L5.90535 11.1553Z M2 5C2 3.34315 3.34315 2 5 2H11C12.6569 2 14 3.34315 14 5V11C14 12.6569 12.6569 14 11 14H5C3.34315 14 2 12.6569 2 11V5ZM11 12.5H5C4.17157 12.5 3.5 11.8284 3.5 11V5C3.5 4.17157 4.17157 3.5 5 3.5H11C11.8284 3.5 12.5 4.17157 12.5 5V11C12.5 11.8284 11.8284 12.5 11 12.5Z",
"item-dashed": "M14 5.75H12.5V5C12.5 4.79385 12.4592 4.601 12.3868 4.42622L13.7724 3.85164C13.919 4.20536 14 4.59323 14 5V5.75Z M5.75 2V3.5H5C4.79385 3.5 4.601 3.54075 4.42622 3.61323L3.85164 2.22764C4.20536 2.08096 4.59323 2 5 2H5.75Z M2.22764 3.85164C2.08096 4.20536 2 4.59323 2 5V5.75H3.5V5C3.5 4.79385 3.54075 4.601 3.61323 4.42622L2.22764 3.85164Z M2 10.25H3.5V11C3.5 11.2062 3.54075 11.399 3.61323 11.5738L2.22764 12.1484C2.08096 11.7946 2 11.4068 2 11V10.25Z M10.25 14V12.5H11C11.2062 12.5 11.399 12.4592 11.5738 12.3868L12.1484 13.7724C11.7946 13.919 11.4068 14 11 14H10.25Z M14 7.25H12.5V8.75H14V7.25Z M14 10.25H12.5V11C12.5 11.2062 12.4592 11.399 12.3868 11.5738L13.7724 12.1484C13.919 11.7946 14 11.4068 14 11V10.25Z M8.75 14V12.5H7.25V14H8.75Z M5.75 14V12.5H5C4.79385 12.5 4.601 12.4592 4.42622 12.3868L3.85164 13.7724C4.20536 13.919 4.59323 14 5 14H5.75Z M2 8.75H3.5V7.25H2V8.75Z M7.25 2V3.5H8.75V2H7.25Z M10.25 2V3.5H11C11.2062 3.5 11.399 3.54075 11.5738 3.61323L12.1484 2.22764C11.7946 2.08096 11.4068 2 11 2H10.25Z",
"item-move": "M11 12.5H5C4.17157 12.5 3.5 11.8284 3.5 11V8.75H8.11127L6.30367 10.4543L7.33269 11.5457L10.5145 8.54569L11.0933 8L10.5145 7.45431L7.33269 4.45431L6.30367 5.54569L8.11127 7.25H3.5V5C3.5 4.17157 4.17157 3.5 5 3.5H11C11.8284 3.5 12.5 4.17157 12.5 5V11C12.5 11.8284 11.8284 12.5 11 12.5ZM2 5C2 3.34315 3.34315 2 5 2H11C12.6569 2 14 3.34315 14 5V11C14 12.6569 12.6569 14 11 14H5C3.34315 14 2 12.6569 2 11V5Z"
"item-move": "M11 12.5H5C4.17157 12.5 3.5 11.8284 3.5 11V8.75H8.11127L6.30367 10.4543L7.33269 11.5457L10.5145 8.54569L11.0933 8L10.5145 7.45431L7.33269 4.45431L6.30367 5.54569L8.11127 7.25H3.5V5C3.5 4.17157 4.17157 3.5 5 3.5H11C11.8284 3.5 12.5 4.17157 12.5 5V11C12.5 11.8284 11.8284 12.5 11 12.5ZM2 5C2 3.34315 3.34315 2 5 2H11C12.6569 2 14 3.34315 14 5V11C14 12.6569 12.6569 14 11 14H5C3.34315 14 2 12.6569 2 11V5Z",
"robot": "M12.3829 3.33776C11.9899 3.1336 11.5223 3.05284 10.8366 3.0209C10.4307 1.8448 9.31402 1 8 1C6.68598 1 5.56928 1.8448 5.1634 3.0209C4.47768 3.05284 4.01011 3.1336 3.61708 3.33776C3.06915 3.62239 2.62239 4.06915 2.33776 4.61708C2.1456 4.987 2.06277 5.42295 2.02706 6.04467C0.872483 6.26574 0 7.28098 0 8.5C0 9.71002 0.859646 10.7193 2.00153 10.9503C2.00923 12.1554 2.05584 12.8402 2.33776 13.3829C2.62239 13.9309 3.06915 14.3776 3.61708 14.6622C4.26729 15 5.12153 15 6.83 15H9.17C10.8785 15 11.7327 15 12.3829 14.6622C12.9309 14.3776 13.3776 13.9309 13.6622 13.3829C13.9442 12.8402 13.9908 12.1554 13.9985 10.9503C15.1404 10.7193 16 9.71002 16 8.5C16 7.28098 15.1275 6.26574 13.9729 6.04467C13.9372 5.42295 13.8544 4.987 13.6622 4.61708C13.3776 4.06915 12.9309 3.62239 12.3829 3.33776ZM6.83 4.5C5.95059 4.5 5.38275 4.50121 4.95083 4.53707C4.53687 4.57145 4.38385 4.62976 4.30854 4.66888C4.03457 4.81119 3.81119 5.03457 3.66888 5.30854C3.62976 5.38385 3.57145 5.53687 3.53707 5.95083C3.50121 6.38275 3.5 6.95059 3.5 7.83V10.17C3.5 11.0494 3.50121 11.6173 3.53707 12.0492C3.57145 12.4631 3.62976 12.6161 3.66888 12.6915C3.81119 12.9654 4.03457 13.1888 4.30854 13.3311C4.38385 13.3702 4.53687 13.4285 4.95083 13.4629C5.38275 13.4988 5.95059 13.5 6.83 13.5H9.17C10.0494 13.5 10.6173 13.4988 11.0492 13.4629C11.4631 13.4285 11.6161 13.3702 11.6915 13.3311C11.9654 13.1888 12.1888 12.9654 12.3311 12.6915C12.3702 12.6161 12.4285 12.4631 12.4629 12.0492C12.4988 11.6173 12.5 11.0494 12.5 10.17V7.83C12.5 6.95059 12.4988 6.38275 12.4629 5.95083C12.4285 5.53687 12.3702 5.38385 12.3311 5.30854C12.1888 5.03457 11.9654 4.81119 11.6915 4.66888C11.6161 4.62976 11.4631 4.57145 11.0492 4.53707C10.6173 4.50121 10.0494 4.5 9.17 4.5H6.83ZM5.75 10C5.33579 10 5 10.3358 5 10.75C5 11.1642 5.33579 11.5 5.75 11.5H10.25C10.6642 11.5 11 11.1642 11 10.75C11 10.3358 10.6642 10 10.25 10H5.75ZM6 9C6.55228 9 7 8.55228 7 8C7 7.44772 6.55228 7 6 7C5.44772 7 5 7.44772 5 8C5 8.55228 5.44772 9 6 9ZM11 8C11 8.55228 10.5523 9 10 9C9.44771 9 9 8.55228 9 8C9 7.44772 9.44771 7 10 7C10.5523 7 11 7.44772 11 8Z",
"doc": "M12 6.75H4V5.25H12V6.75Z M4 9.75H8V8.25H4V9.75Z M4 1C2.34315 1 1 2.34315 1 4V12C1 13.6569 2.34315 15 4 15H12C13.6569 15 15 13.6569 15 12V4C15 2.34315 13.6569 1 12 1H4ZM2.5 12C2.5 12.8284 3.17157 13.5 4 13.5H12C12.8284 13.5 13.5 12.8284 13.5 12V4C13.5 3.17157 12.8284 2.5 12 2.5H4C3.17157 2.5 2.5 3.17157 2.5 4V12Z"
}

View File

@ -1,5 +1,9 @@
export function useAutoHeight(element: HTMLTextAreaElement) {
if (!element) return;
const elementBorder =
parseInt(getComputedStyle(element).borderTopWidth) +
parseInt(getComputedStyle(element).borderBottomWidth);
element.style.height = 'auto';
element.style.height = `${element.scrollHeight}px`;
element.style.height = `${element.scrollHeight + elementBorder}px`;
}

View File

@ -1,6 +1,7 @@
<script lang="ts">
import '../styles/main.postcss';
import { PromptService as AIPromptService } from '$lib/ai/promptService';
import { AIService } from '$lib/ai/service';
import { AuthService } from '$lib/backend/auth';
import { GitConfigService } from '$lib/backend/gitConfigService';
@ -44,6 +45,7 @@
setContext(HttpClient, data.cloud);
setContext(User, data.userService.user);
setContext(RemotesService, data.remotesService);
setContext(AIPromptService, data.aiPromptService);
let shareIssueModal: ShareIssueModal;

View File

@ -1,3 +1,4 @@
import { PromptService as AIPromptService } from '$lib/ai/promptService';
import { AIService } from '$lib/ai/service';
import { initAnalyticsIfEnabled } from '$lib/analytics/analytics';
import { AuthService } from '$lib/backend/auth';
@ -54,6 +55,7 @@ export async function load() {
const gitConfig = new GitConfigService();
const aiService = new AIService(gitConfig, httpClient);
const remotesService = new RemotesService();
const aiPromptService = new AIPromptService();
return {
authService,
@ -67,6 +69,7 @@ export async function load() {
remoteUrl$,
gitConfig,
aiService,
remotesService
remotesService,
aiPromptService
};
}

View File

@ -2,6 +2,7 @@
import { AIService, GitAIConfigKey, KeyOption } from '$lib/ai/service';
import { OpenAIModelName, AnthropicModelName, ModelKind } from '$lib/ai/types';
import { GitConfigService } from '$lib/backend/gitConfigService';
import AiPromptEdit from '$lib/components/AIPromptEdit/AIPromptEdit.svelte';
import InfoMessage from '$lib/components/InfoMessage.svelte';
import RadioButton from '$lib/components/RadioButton.svelte';
import SectionCard from '$lib/components/SectionCard.svelte';
@ -11,6 +12,7 @@
import TextBox from '$lib/components/TextBox.svelte';
import WelcomeSigninAction from '$lib/components/WelcomeSigninAction.svelte';
import ContentWrapper from '$lib/components/settings/ContentWrapper.svelte';
import Section from '$lib/components/settings/Section.svelte';
import { UserService } from '$lib/stores/user';
import { getContext } from '$lib/utils/context';
import { onMount, tick } from 'svelte';
@ -19,7 +21,6 @@
const aiService = getContext(AIService);
const userService = getContext(UserService);
const user = userService.user;
let initialized = false;
let modelKind: ModelKind | undefined;
@ -322,17 +323,43 @@
</svelte:fragment>
</SectionCard>
<style>
.ai-settings__text {
color: var(--clr-text-2);
margin-bottom: var(--size-12);
}
<Spacer />
.inputs-group {
display: flex;
flex-direction: column;
gap: var(--size-16);
width: 100%;
}
</style>
<Section>
<svelte:fragment slot="title">Custom AI prompts</svelte:fragment>
<svelte:fragment slot="description">
GitButler's AI assistant generates commit messages and branch names. Use default prompts or
create your own. Assign prompts in the <button
class="link"
on:click={() => console.log('got to project settings')}>project settings</button
>.
</svelte:fragment>
<div class="prompt-groups">
<AiPromptEdit promptUse="commits" />
<Spacer margin={12} />
<AiPromptEdit promptUse="branches" />
</div>
</Section>
</ContentWrapper>
<style>
.ai-settings__text {
color: var(--clr-text-2);
margin-bottom: var(--size-12);
}
.inputs-group {
display: flex;
flex-direction: column;
gap: var(--size-16);
width: 100%;
}
.prompt-groups {
display: flex;
flex-direction: column;
gap: var(--size-12);
margin-top: var(--size-16);
}
</style>

View File

@ -78,6 +78,43 @@ button {
}
}
/* custom scrollbar */
.scrollbar,
pre {
&::-webkit-scrollbar {
background-color: transaparent;
width: var(--size-14);
}
&::-webkit-scrollbar-track {
background-color: transaparent;
}
&::-webkit-scrollbar-thumb {
background-color: var(--clr-border-1);
background-clip: padding-box;
border-radius: var(--size-12);
border: var(--size-4) solid rgba(0, 0, 0, 0);
opacity: 0.3;
}
&::-webkit-scrollbar-thumb:hover {
opacity: 0.8;
}
&::-webkit-scrollbar-button {
display: none;
}
}
.link {
text-decoration: underline;
&:hover {
text-decoration: none;
}
}
/**
* Prevents elements within drop-zones from firing mouse events, making
* it much easier to manage in/out/over/leave events since they fire less

View File

@ -1,19 +1,32 @@
.markdown {
& p:last-child,
& ul:last-child,
& ol:last-child,
& blockquote:last-child,
& pre:last-child,
& hr:last-child {
margin-bottom: 0;
}
h1 {
font-size: 2em;
margin-bottom: 0.8em;
}
h2 {
font-size: 1.5em;
margin-bottom: 0.8em;
}
h3 {
font-size: 1.17em;
margin-bottom: 0.8em;
}
h4 {
font-size: 1em;
margin-bottom: 0.8em;
}
p {
margin: 1em 0;
margin-bottom: 1.2em;
}
ul {
@ -44,6 +57,8 @@
padding: 1em;
background-color: var(--clr-scale-ntrl-90);
border: 1px solid var(--clr-scale-ntrl-70);
overflow: auto;
border-radius: var(--radius-m);
}
code {
@ -61,6 +76,8 @@
box-sizing: content-box;
height: 0;
overflow: visible;
margin: 2em 0;
opacity: 0.2;
}
b,

View File

@ -192,7 +192,6 @@
--radius-l: 0.75rem;
--radius-m: 0.375rem;
--radius-s: 0.25rem;
--size-1: 0.06125rem;
--size-2: 0.125rem;
--size-4: 0.25rem;
--size-6: 0.375rem;
@ -309,27 +308,27 @@
--clr-text-1: var(--clr-core-ntrl-95);
--clr-text-2: var(--clr-core-ntrl-50);
--clr-text-3: var(--clr-core-ntrl-40);
--clr-theme-err-bg: var(--clr-core-err-20);
--clr-theme-err-bg: var(--clr-core-err-10);
--clr-theme-err-element: var(--clr-core-err-40);
--clr-theme-err-on-element: var(--clr-core-err-90);
--clr-theme-err-on-soft: var(--clr-core-err-80);
--clr-theme-err-soft: var(--clr-core-err-20);
--clr-theme-pop-bg: var(--clr-core-pop-20);
--clr-theme-pop-bg: var(--clr-core-pop-10);
--clr-theme-pop-element: var(--clr-core-pop-40);
--clr-theme-pop-on-element: var(--clr-core-ntrl-100);
--clr-theme-pop-on-soft: var(--clr-core-pop-80);
--clr-theme-pop-soft: var(--clr-core-pop-20);
--clr-theme-purp-bg: var(--clr-core-purp-20);
--clr-theme-purp-bg: var(--clr-core-purp-10);
--clr-theme-purp-element: var(--clr-core-purp-40);
--clr-theme-purp-on-element: var(--clr-core-purp-90);
--clr-theme-purp-on-soft: var(--clr-core-purp-80);
--clr-theme-purp-soft: var(--clr-core-purp-20);
--clr-theme-succ-bg: var(--clr-core-succ-20);
--clr-theme-succ-bg: var(--clr-core-succ-10);
--clr-theme-succ-element: var(--clr-core-succ-40);
--clr-theme-succ-on-element: var(--clr-core-succ-90);
--clr-theme-succ-on-soft: var(--clr-core-succ-80);
--clr-theme-succ-soft: var(--clr-core-succ-20);
--clr-theme-warn-bg: var(--clr-core-warn-20);
--clr-theme-warn-bg: var(--clr-core-warn-10);
--clr-theme-warn-element: var(--clr-core-warn-40);
--clr-theme-warn-on-element: var(--clr-core-warn-90);
--clr-theme-warn-on-soft: var(--clr-core-warn-80);