feat: new ai design (#3152)

* feat: new ai design

* fix: show caret + esc to cancel
This commit is contained in:
HugoCasa
2024-02-06 15:13:20 +01:00
committed by GitHub
parent 1352add8c6
commit 58d34845a0
14 changed files with 312 additions and 154 deletions
+9 -31
View File
@@ -522,7 +522,7 @@
</Button>
{/if}
{#if collabMode}
<div class="flex items-center px-1">
<div class="flex items-center px-3">
<Toggle
options={{ right: '' }}
size="xs"
@@ -531,13 +531,13 @@
/>
<Popover>
<svelte:fragment slot="text">Multiplayer</svelte:fragment>
<Users class="ml-1" size={12} />
<Users class="ml-1 text-tertiary" size={14} />
</Popover>
{#if collabLive}
<button
title="Show invite link"
class="p-1 rounded hover:bg-gray-400 mx-1 border"
on:click={() => dispatch('collabPopup')}><Link size={12} /></button
on:click={() => dispatch('collabPopup')}><Link size={14} /></button
>
<div class="isolate flex -space-x-2 pl-2">
{#each collabUsers as user}
@@ -555,35 +555,13 @@
</div>
{/if}
<ScriptGen {editor} {diffEditor} {lang} {iconOnly} {args} />
<CodeCompletionStatus />
<FormatOnSave />
<!-- <Popover
notClickable
placement="bottom"
disappearTimeout={0}
class="px-1"
disablePopup={!iconOnly}
>
<Button
btnClasses="!font-medium"
size="xs"
spacingSize="md"
color="light"
on:click={editor?.format}
{iconOnly}
startIcon={{ icon: faBroom }}
>
Format ({getModifierKey()}+S)
</Button>
<svelte:fragment slot="text">
Format <Kbd class="!text-gray-800">{getModifierKey()}</Kbd> + <Kbd class="!text-gray-800">
S
</Kbd>
</svelte:fragment>
</Popover> -->
<div class="flex flex-row gap-1">
<ScriptGen {editor} {diffEditor} {lang} {iconOnly} {args} />
<CodeCompletionStatus />
</div>
</div>
</div>
@@ -4,6 +4,7 @@
import { formatOnSave } from '$lib/stores'
import Popover from './Popover.svelte'
import { getLocalSetting, storeLocalSetting } from '$lib/utils'
import PaintbrushOff from './icons/PaintbrushOff.svelte'
const SETTING_NAME = 'formatOnSave'
function loadSetting() {
@@ -23,8 +24,12 @@
>{$formatOnSave ? 'Disable' : 'Enable'} auto-formatting</svelte:fragment
>
<Button
size="xs"
color="light"
startIcon={{ icon: Paintbrush, classes: !$formatOnSave ? 'text-red-400' : '' }}
startIcon={{
icon: !$formatOnSave ? PaintbrushOff : Paintbrush
}}
btnClasses="text-tertiary"
on:click={() => {
storeSetting()
}}
@@ -418,6 +418,9 @@
code={script.content}
configName="summary"
let:updateFocus
let:active
let:classNames
focused
on:change={() => {
if (initialPath == '' && script.summary?.length > 0 && !dirtyPath) {
path?.setName(
@@ -433,10 +436,11 @@
<input
type="text"
autofocus
class={classNames}
on:focus={() => updateFocus(true)}
on:blur={() => updateFocus(false)}
bind:value={script.summary}
placeholder={'Short summary to be displayed when listed'}
placeholder={!active ? 'Short summary to be displayed when listed' : ''}
/>
</MetadataGen>
<Label label="Path">
@@ -459,15 +463,17 @@
el={descriptionTextArea}
label="Description"
let:updateFocus
let:active
let:classNames
>
<textarea
on:focus={() => updateFocus(true)}
on:blur={() => updateFocus(false)}
class={classNames}
use:autosize
bind:this={descriptionTextArea}
bind:value={script.description}
placeholder={'Description displayed in the details page'}
class="text-sm"
placeholder={!active ? 'Description displayed in the details page' : ''}
/>
</MetadataGen>
</div>
+62 -64
View File
@@ -22,68 +22,66 @@
const bothOptions = Boolean(options.left) && Boolean(options.right)
</script>
<span class="{$$props.class} z-auto">
<label
for={id}
class="inline-flex items-center mt-2 duration-50 {disabled
? 'grayscale opacity-50'
: 'cursor-pointer'}"
>
{#if Boolean(options?.left)}
<span
class={twMerge(
'mr-2 font-medium duration-50',
bothOptions ? (checked ? 'text-disabled' : 'text-primary') : 'text-primary',
size === 'xs' ? 'text-xs' : 'text-sm',
textClass
)}
style={textStyle}
>
{options?.left}
</span>
{/if}
<label
for={id}
class="{$$props.class || ''} z-auto inline-flex items-center duration-50 {disabled
? 'grayscale opacity-50'
: 'cursor-pointer'}"
>
{#if Boolean(options?.left)}
<span
class={twMerge(
'mr-2 font-medium duration-50',
bothOptions ? (checked ? 'text-disabled' : 'text-primary') : 'text-primary',
size === 'xs' ? 'text-xs' : 'text-sm',
textClass
)}
style={textStyle}
>
{options?.left}
</span>
{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="relative" on:click|stopPropagation>
<input
on:focus
on:click
{disabled}
type="checkbox"
{id}
class="sr-only peer"
bind:checked
on:change|stopPropagation={(e) => {
dispatch('change', checked)
}}
/>
<div
class={classNames(
"transition-all bg-surface-selected rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:bg-surface after:border-white after:border after:rounded-full after:transition-all ",
color == 'red' ? 'peer-checked:bg-red-600' : 'peer-checked:bg-blue-600',
size === 'sm'
? 'w-11 h-6 after:top-0.5 after:left-[2px] after:h-5 after:w-5'
: 'w-7 h-4 after:top-0.5 after:left-[2px] after:h-3 after:w-3'
)}
/>
</div>
{#if Boolean(options?.right)}
<span
class={twMerge(
'ml-2 font-medium duration-50',
bothOptions ? (checked ? 'text-primary' : 'text-disabled') : 'text-primary',
size === 'xs' ? 'text-xs' : 'text-sm',
textClass
)}
style={textStyle}
>
{options?.right}
{#if options?.rightTooltip}
<Tooltip>{options?.rightTooltip}</Tooltip>
{/if}
</span>
{/if}
<slot name="right" />
</label>
</span>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="relative" on:click|stopPropagation>
<input
on:focus
on:click
{disabled}
type="checkbox"
{id}
class="sr-only peer"
bind:checked
on:change|stopPropagation={(e) => {
dispatch('change', checked)
}}
/>
<div
class={classNames(
"transition-all bg-surface-selected rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:bg-surface after:border-white after:border after:rounded-full after:transition-all ",
color == 'red' ? 'peer-checked:bg-red-600' : 'peer-checked:bg-blue-600',
size === 'sm'
? 'w-11 h-6 after:top-0.5 after:left-[2px] after:h-5 after:w-5'
: 'w-7 h-4 after:top-0.5 after:left-[2px] after:h-3 after:w-3'
)}
/>
</div>
{#if Boolean(options?.right)}
<span
class={twMerge(
'ml-2 font-medium duration-50',
bothOptions ? (checked ? 'text-primary' : 'text-disabled') : 'text-primary',
size === 'xs' ? 'text-xs' : 'text-sm',
textClass
)}
style={textStyle}
>
{options?.right}
{#if options?.rightTooltip}
<Tooltip>{options?.rightTooltip}</Tooltip>
{/if}
</span>
{/if}
<slot name="right" />
</label>
@@ -1,9 +1,10 @@
<script lang="ts">
import { ZapIcon, ZapOffIcon } from 'lucide-svelte'
import { SparklesIcon } from 'lucide-svelte'
import Button from '../common/button/Button.svelte'
import { codeCompletionLoading, copilotInfo, codeCompletionSessionEnabled } from '$lib/stores'
import Popover from '../Popover.svelte'
import { getLocalSetting, storeLocalSetting } from '$lib/utils'
import SparklesOffIcon from '../icons/SparklesOffIcon.svelte'
const SETTING_NAME = 'codeCompletionSessionEnabled'
function loadSetting() {
@@ -21,19 +22,26 @@
{#if $copilotInfo.exists_openai_resource_path && $copilotInfo.code_completion_enabled}
<Popover>
<svelte:fragment slot="text"
>{$codeCompletionSessionEnabled ? 'Disable' : 'Enable'} code completion (applies only to you)</svelte:fragment
>Code completion: {$codeCompletionSessionEnabled ? 'disabled' : 'enabled'} (applies only to you)</svelte:fragment
>
<Button
color="light"
size="xs"
loading={$codeCompletionLoading}
startIcon={$codeCompletionLoading
? undefined
: {
icon: $codeCompletionSessionEnabled ? ZapIcon : ZapOffIcon
icon: $codeCompletionSessionEnabled ? SparklesIcon : SparklesOffIcon,
classes: $codeCompletionSessionEnabled
? 'text-sky-900 dark:text-sky-200'
: 'text-sky-900/50 dark:text-sky-200/50'
}}
on:click={() => {
storeSetting()
}}
btnClasses={$codeCompletionSessionEnabled
? 'bg-sky-50 dark:bg-gray-700 dark:hover:bg-surface-hover'
: ''}
/>
</Popover>
{:else}
@@ -44,7 +52,7 @@
<Button
color="light"
startIcon={{
icon: ZapOffIcon
icon: SparklesOffIcon
}}
disabled
/>
@@ -62,7 +62,7 @@
startIcon={{ icon: Wand2 }}
iconOnly
title="AI Assistant"
btnClasses="min-h-[30px]"
btnClasses="min-h-[30px] text-sky-900 dark:text-sky-200 bg-sky-100 dark:bg-gray-700 hover:bg-surface-hover dark:hover:bg-surface-hover"
loading={genLoading}
clickableWhileLoading
on:click={genLoading ? () => abortController?.abort() : undefined}
@@ -87,7 +87,7 @@
color="dark"
variant="contained"
buttonType="button"
btnClasses="!p-1 !w-[38px] !ml-2"
btnClasses="!p-1 !w-[38px] !ml-2 text-sky-900 dark:text-sky-200 bg-sky-100 dark:bg-gray-700 hover:bg-surface-hover dark:hover:bg-surface-hover"
title="Generate CRON schedule from prompt"
aria-label="Generate"
iconOnly
@@ -10,6 +10,7 @@
import type { FlowModule } from '$lib/gen'
import type { FlowEditorContext } from '../flows/types'
import { ExternalLink, Wand2 } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
export let copilotLoading: boolean
export let copilotStatus: string
@@ -33,7 +34,12 @@
<ManualPopover bind:this={copilotPopover}>
<Button
size="xs"
btnClasses={$currentStepStore !== undefined ? 'z-[901]' : ''}
btnClasses={twMerge(
$currentStepStore !== undefined ? 'z-[901]' : '',
copilotLoading || ($currentStepStore !== undefined && $currentStepStore !== 'Input')
? ''
: 'bg-sky-100 dark:bg-gray-700 text-sky-900 dark:text-sky-200 dark:hover:bg-surface-hover'
)}
on:click={() => {
if (copilotLoading || ($currentStepStore !== undefined && $currentStepStore !== 'Input')) {
abortController?.abort()
@@ -119,6 +125,7 @@
startIcon={{
icon: Wand2
}}
btnClasses="bg-sky-100 dark:bg-gray-700 text-sky-900 dark:text-sky-200 dark:hover:bg-surface-hover"
color={'light'}
variant={'border'}
nonCaptureEvent
@@ -6,7 +6,7 @@
import type { ChatCompletionMessageParam } from 'openai/resources'
import { copilotInfo, metadataCompletionEnabled } from '$lib/stores'
import Label from '../Label.svelte'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, onDestroy } from 'svelte'
import { sendUserToast } from '$lib/toast'
type Config = {
@@ -58,10 +58,14 @@ The description should focus on what it does and should not contain what concept
export let el: HTMLElement | undefined = undefined
export let label: string
let generatedContent = ''
let genEl: HTMLElement | undefined = undefined
let active = false
let loading = false
let abortController = new AbortController()
let manualDisabled = false
let focused = false
export let focused = false
const updateFocus = (val) => {
focused = val
}
@@ -83,13 +87,12 @@ The description should focus on what it does and should not contain what concept
}
]
const response = await getCompletion(messages, abortController)
content = ''
generatedContent = ''
for await (const chunk of response) {
const toks = chunk.choices[0]?.delta?.content || ''
content += toks
if (el !== undefined) {
el.style.height = 'auto'
el.style.height = el.scrollHeight + 'px'
generatedContent += toks
if (el !== undefined && genEl !== undefined) {
el.style.height = Math.max(genEl.scrollHeight + 34, 58) + 'px'
}
}
} catch (err) {
@@ -121,46 +124,87 @@ The description should focus on what it does and should not contain what concept
$: if (content) {
dispatch('change', { content })
}
$: active =
$copilotInfo.exists_openai_resource_path &&
$metadataCompletionEnabled &&
!content &&
(loading || focused || !!generatedContent) &&
!manualDisabled
$: focused && (manualDisabled = false)
onDestroy(() => {
abortController.abort()
})
</script>
<div class="relative">
<div class="flex flex-row" />
<!-- svelte-ignore a11y-no-static-element-interactions -->
<Label {label}>
<div slot="header" class="flex flex-row pl-1 gap-2 items-center">
{#if $copilotInfo.exists_openai_resource_path && $metadataCompletionEnabled}
{#if loading}
<Loader2 class="animate-spin text-gray-400" size={18} />
<span class="text-xs">
<span class="border px-1 py-0.5 rounded-md text-2xs text-bold bg-white text-black">
ESC
</span> to cancel
</span>
{:else if !content && focused}
<span class="text-xs">
<span class="border px-1 py-0.5 rounded-md text-2xs text-bold bg-white text-black">
TAB
</span> to generate
</span>
{/if}
{/if}
</div>
<div
class="relative"
on:keydown={(event) => {
if (!$copilotInfo.exists_openai_resource_path || !$metadataCompletionEnabled) {
return
}
if (event.key === 'Tab' && !loading && !content) {
event.preventDefault()
generateContent()
} else if (event.key === 'Escape' && loading) {
if (event.key === 'Tab') {
if (!loading && generatedContent) {
event.preventDefault()
content = generatedContent
generatedContent = ''
} else if (!loading && !content) {
event.preventDefault()
generateContent()
}
} else if (event.key === 'Escape') {
event.preventDefault()
event.stopPropagation()
abortController.abort()
if (loading) {
abortController.abort()
} else {
manualDisabled = true
}
}
}}
>
<slot {updateFocus} />
<div
class={'absolute left-0.5 flex flex-row pl-1 gap-2 items-start top-[0.3rem] pointer-events-none'}
>
{#if active}
<span
class="absolute text-xs text-sky-900 bg-sky-100 dark:text-sky-200 dark:bg-gray-700 p-1 rounded-md flex flex-row items-center justify-center gap-2 w-32 shrink-0"
>
{#if loading}
<Loader2 class="animate-spin text-gray-500 dark:text-gray-400" size={16} />
{/if}
<div>
<span class="px-1 py-0.5 rounded-md text-2xs text-bold bg-white dark:bg-surface">
{#if loading}
ESC
{:else}
TAB
{/if}
</span>
{#if loading}
to cancel
{:else if generatedContent}
to accept
{:else}
to generate
{/if}
</div>
</span>
<span
class="text-sm leading-6 indent-[8.5rem] text-gray-500 dark:text-gray-400 pr-1"
bind:this={genEl}
>
{generatedContent}
</span>
{/if}
</div>
<slot {updateFocus} {active} classNames={active && !generatedContent ? '!pl-[8.7rem]' : ''} />
</div>
</Label>
</div>
@@ -1,9 +1,10 @@
<script lang="ts">
import { ExternalLink, ZapIcon, ZapOffIcon } from 'lucide-svelte'
import { ExternalLink, Sparkles, ZapOffIcon } from 'lucide-svelte'
import Button from '../common/button/Button.svelte'
import { copilotInfo, metadataCompletionEnabled } from '$lib/stores'
import Popover from '../Popover.svelte'
import { getLocalSetting, storeLocalSetting } from '$lib/utils'
import SparklesOffIcon from '../icons/SparklesOffIcon.svelte'
const SETTING_NAME = 'metadataCompletionEnabled'
function loadSetting() {
@@ -21,16 +22,21 @@
{#if $copilotInfo.exists_openai_resource_path}
<Popover>
<svelte:fragment slot="text"
>{$metadataCompletionEnabled ? 'Disable' : 'Enable'} metadata completion (applies only to you)</svelte:fragment
>Metadata completion: {$metadataCompletionEnabled ? 'disabled' : 'enabled'} (applies only to you)</svelte:fragment
>
<Button
size="xs"
color="light"
startIcon={{
icon: $metadataCompletionEnabled ? ZapIcon : ZapOffIcon
icon: $metadataCompletionEnabled ? Sparkles : SparklesOffIcon,
classes: $metadataCompletionEnabled
? 'text-sky-900 dark:text-sky-200'
: 'text-sky-900/50 dark:text-sky-200/50'
}}
on:click={() => {
storeSetting()
}}
btnClasses={$metadataCompletionEnabled ? 'bg-sky-100 dark:bg-gray-700' : ''}
/>
</Popover>
{:else}
@@ -59,11 +59,13 @@
await sleep(300)
showDiff()
} catch (err) {
if (err?.message) {
sendUserToast('Failed to generate code: ' + err.message, true)
} else {
sendUserToast('Failed to generate code', true)
console.error(err)
if (!abortController?.signal.aborted) {
if (err?.message) {
sendUserToast('Failed to generate code: ' + err.message, true)
} else {
sendUserToast('Failed to generate code', true)
console.error(err)
}
}
closePopup()
} finally {
@@ -165,6 +167,9 @@
startIcon={genLoading ? undefined : { icon: Wand2 }}
nonCaptureEvent={!genLoading}
on:click={genLoading ? () => abortController?.abort() : undefined}
btnClasses={genLoading
? ''
: 'text-sky-900 bg-sky-100 dark:text-sky-200 dark:bg-gray-700 hover:bg-surface-hover dark:hover:bg-surface-hover'}
>{#if genLoading}
<WindmillIcon
white
@@ -19,9 +19,10 @@
import LoadingIcon from '../apps/svelte-select/lib/LoadingIcon.svelte'
import { sleep } from '$lib/utils'
import { autoPlacement } from '@floating-ui/core'
import { Ban, Check, ExternalLink, HistoryIcon, Wand2, X, ZapIcon } from 'lucide-svelte'
import { Ban, Bot, Check, ExternalLink, HistoryIcon, Wand2, X } from 'lucide-svelte'
import { fade } from 'svelte/transition'
import { isInitialCode } from '$lib/script_helpers'
import { twMerge } from 'tailwind-merge'
// props
export let iconOnly: boolean = false
@@ -97,6 +98,7 @@
}
} finally {
genLoading = false
blockPopupOpen = false
}
}
@@ -270,22 +272,31 @@
color={genLoading ? 'red' : 'light'}
btnClasses={genLoading
? '!px-3 z-[5000]'
: '!px-2 !bg-surface-secondary hover:!bg-surface-hover'}
: '!px-2 bg-sky-50 dark:bg-gray-700 dark:hover:bg-surface-hover'}
nonCaptureEvent={!genLoading}
on:click={genLoading ? () => abortController?.abort() : undefined}
bind:element={button}
iconOnly
title="Generate code from Prompt"
startIcon={{ icon: genLoading ? Ban : Wand2 }}
startIcon={genLoading
? { icon: Ban }
: { icon: Wand2, classes: 'text-sky-900 dark:text-sky-200 ' }}
/>
{:else}
<Button
title="Generate code from prompt"
btnClasses={'!font-medium ' + (genLoading ? 'z-[5000]' : '')}
btnClasses={twMerge(
'!font-medium',
genLoading
? 'z-[5000]'
: 'text-sky-900 dark:text-sky-200 bg-sky-50 dark:bg-gray-700 dark:hover:bg-surface-hover'
)}
size="xs"
color={genLoading ? 'red' : 'light'}
spacingSize="md"
startIcon={genLoading ? undefined : { icon: Wand2 }}
startIcon={genLoading
? undefined
: { icon: Wand2, classes: 'text-sky-900 dark:text-sky-200' }}
propagateEvent
on:click={genLoading
? () => abortController?.abort()
@@ -299,6 +310,7 @@
}
}}
bind:element={button}
{iconOnly}
>
{#if genLoading}
<WindmillIcon
@@ -310,7 +322,7 @@
/>
Stop
{:else}
AI
AI Gen
{/if}
</Button>
{/if}
@@ -335,7 +347,7 @@
</ToggleButtonGroup>
<div class="text-[0.6rem] text-secondary opacity-60 flex flex-row items-center gap-0.5">
GPT-4 Turbo<ZapIcon size={14} />
GPT-4 Turbo<Bot size={14} />
</div>
</div>
<div class="flex w-96">
@@ -356,7 +368,7 @@
size="xs"
color="blue"
buttonType="button"
btnClasses="!p-1 !w-[38px] !ml-2"
btnClasses="!p-1 !w-[38px] !ml-2 text-sky-900 dark:text-sky-200 bg-sky-100 dark:bg-gray-700 hover:bg-surface-hover dark:hover:bg-surface-hover"
title="Generate code from prompt"
aria-label="Generate"
on:click={() => {
@@ -170,7 +170,7 @@
: undefined}
slot="trigger"
type="button"
class="text-primary bg-surface border mx-0.5 focus:outline-none hover:bg-surface-hover focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-8 h-8 flex items-center justify-center"
class=" bg-sky-100 dark:bg-gray-700 text-sky-900 dark:text-sky-200 dark:hover:bg-surface-hover border mx-0.5 focus:outline-none hover:bg-surface-hover focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-8 h-8 flex items-center justify-center"
>
<Wand2 size={16} />
</button>
@@ -0,0 +1,34 @@
<script lang="ts">
export let size = 24
</script>
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M18.37 2.62999L14 6.99999L12.41 5.40999C12.0353 5.03749 11.5284 4.8284 11 4.8284C10.4716 4.8284 9.96473 5.03749 9.59 5.40999L8 6.99999L17 16L18.59 14.41C18.9625 14.0353 19.1716 13.5284 19.1716 13C19.1716 12.4716 18.9625 11.9647 18.59 11.59L17 9.99999L21.37 5.62999C21.7678 5.23216 21.9913 4.6926 21.9913 4.12999C21.9913 3.56738 21.7678 3.02781 21.37 2.62999C20.9722 2.23216 20.4326 2.00867 19.87 2.00867C19.3074 2.00867 18.7678 2.23216 18.37 2.62999Z"
stroke="currentcolor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M9 8C7 11 5 11.5 2 12L10 22C12 21 16 17 16 15"
stroke="currentcolor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M14.5 17.5L4.5 15"
stroke="currentcolor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M22 22L2 2"
stroke="currentcolor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
@@ -0,0 +1,55 @@
<script lang="ts">
export let size = '24'
</script>
<svg
width={size}
height={size}
class={$$props.class}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 3L10.088 8.813C9.99015 9.11051 9.82379 9.38088 9.60234 9.60234C9.38088 9.82379 9.11051 9.99015 8.813 10.088L3 12L8.813 13.912C9.11051 14.0099 9.38088 14.1762 9.60234 14.3977C9.82379 14.6191 9.99015 14.8895 10.088 15.187L12 21L13.912 15.187C14.0099 14.8895 14.1762 14.6191 14.3977 14.3977C14.6191 14.1762 14.8895 14.0099 15.187 13.912L21 12L15.187 10.088C14.8895 9.99015 14.6191 9.82379 14.3977 9.60234C14.1762 9.38088 14.0099 9.11051 13.912 8.813L12 3Z"
stroke="currentcolor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M5 3V7"
stroke="currentcolor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M19 17V21"
stroke="currentcolor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M3 5H7"
stroke="currentcolor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M17 19H21"
stroke="currentcolor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M2 22L22 2"
stroke="currentcolor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>