PromptCommands.svelte 4.05 KB
Newer Older
Timothy J. Baek's avatar
Timothy J. Baek committed
1
<script lang="ts">
Timothy J. Baek's avatar
Timothy J. Baek committed
2
	import { prompts } from '$lib/stores';
Timothy J. Baek's avatar
Timothy J. Baek committed
3
	import { findWordIndices } from '$lib/utils';
4
	import { tick, getContext } from 'svelte';
Jannik Streidl's avatar
Jannik Streidl committed
5
	import { toast } from 'svelte-sonner';
Timothy J. Baek's avatar
Timothy J. Baek committed
6

7
8
	const i18n = getContext('i18n');

9
	export let files;
Timothy J. Baek's avatar
Timothy J. Baek committed
10
11
12
13
	export let prompt = '';
	let selectedCommandIdx = 0;
	let filteredPromptCommands = [];

Timothy J. Baek's avatar
Timothy J. Baek committed
14
	$: filteredPromptCommands = $prompts
15
16
		.filter((p) => p.command.includes(prompt))
		.sort((a, b) => a.title.localeCompare(b.title));
Timothy J. Baek's avatar
Timothy J. Baek committed
17
18
19
20
21
22
23
24
25
26
27
28
29
30

	$: 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) => {
31
32
33
34
		let text = command.content;

		if (command.content.includes('{{CLIPBOARD}}')) {
			const clipboardText = await navigator.clipboard.readText().catch((err) => {
35
				toast.error($i18n.t('Failed to read clipboard contents'));
36
37
38
				return '{{CLIPBOARD}}';
			});

39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
			console.log(clipboardText);

			const clipboardItems = await navigator.clipboard.read();

			let imageUrl = null;
			for (const item of clipboardItems) {
				// Check for known image types
				for (const type of item.types) {
					if (type.startsWith('image/')) {
						const blob = await item.getType(type);
						imageUrl = URL.createObjectURL(blob);
						console.log(`Image URL (${type}): ${imageUrl}`);
					}
				}
			}

			if (imageUrl) {
				files = [
					...files,
					{
						type: 'image',
						url: imageUrl
					}
				];
			}

65
66
67
68
			text = command.content.replaceAll('{{CLIPBOARD}}', clipboardText);
		}

		prompt = text;
Timothy J. Baek's avatar
Timothy J. Baek committed
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

		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}
Timothy J. Baek's avatar
Timothy J. Baek committed
91
	<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
Timothy J. Baek's avatar
Timothy J. Baek committed
92
93
		<div class="flex w-full px-2">
			<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
Timothy J. Baek's avatar
Timothy J. Baek committed
94
				<div class=" text-lg font-semibold mt-2">/</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
95
			</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
96

97
			<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white dark:bg-gray-850">
Timothy J. Baek's avatar
Timothy J. Baek committed
98
				<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
Timothy J. Baek's avatar
Timothy J. Baek committed
99
100
					{#each filteredPromptCommands as command, commandIdx}
						<button
Timothy J. Baek's avatar
Timothy J. Baek committed
101
							class=" px-3 py-1.5 rounded-xl w-full text-left {commandIdx === selectedCommandIdx
102
								? ' bg-gray-100 dark:bg-gray-600 selected-command-option-button'
Timothy J. Baek's avatar
Timothy J. Baek committed
103
104
105
106
107
108
109
110
111
112
								: ''}"
							type="button"
							on:click={() => {
								confirmCommand(command);
							}}
							on:mousemove={() => {
								selectedCommandIdx = commandIdx;
							}}
							on:focus={() => {}}
						>
113
							<div class=" font-medium text-black dark:text-gray-100">
Timothy J. Baek's avatar
Timothy J. Baek committed
114
115
116
								{command.command}
							</div>

117
							<div class=" text-xs text-gray-600 dark:text-gray-100">
Timothy J. Baek's avatar
Timothy J. Baek committed
118
119
120
121
122
123
								{command.title}
							</div>
						</button>
					{/each}
				</div>

Timothy J. Baek's avatar
Timothy J. Baek committed
124
				<div
125
					class=" px-2 pb-1 text-xs text-gray-600 dark:text-gray-100 bg-white dark:bg-gray-850 rounded-br-xl flex items-center space-x-1"
Timothy J. Baek's avatar
Timothy J. Baek committed
126
				>
Timothy J. Baek's avatar
Timothy J. Baek committed
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
					<div>
						<svg
							xmlns="http://www.w3.org/2000/svg"
							fill="none"
							viewBox="0 0 24 24"
							stroke-width="1.5"
							stroke="currentColor"
							class="w-3 h-3"
						>
							<path
								stroke-linecap="round"
								stroke-linejoin="round"
								d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
							/>
						</svg>
					</div>

					<div class="line-clamp-1">
145
146
147
						{$i18n.t(
							'Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.'
						)}
Timothy J. Baek's avatar
Timothy J. Baek committed
148
149
					</div>
				</div>
Timothy J. Baek's avatar
Timothy J. Baek committed
150
151
152
153
			</div>
		</div>
	</div>
{/if}