Merge branch 'feat/mamba-d1' of github.com:supabase/supabase into feat/mamba-d1

This commit is contained in:
Terry Sutton committed 2025-03-31 09:53:45 -02:30
commit 26500b019a
20 files changed
+200 -71

No files matched your search

@@ -1,8 +1,10 @@
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { useAppStateSnapshot } from 'state/app-state'
import { AiIconAnimation, Button } from 'ui'
const AssistantButton = () => {
const snap = useAiAssistantStateSnapshot()
const { setEditorPanel } = useAppStateSnapshot()
return (
<Button
@@ -12,6 +14,7 @@ const AssistantButton = () => {
className="h-full w-full rounded-none"
onClick={() => {
snap.toggleAssistant()
setEditorPanel({ open: false })
}}
>
<AiIconAnimation allowHoverEffect size={20} />
@@ -1,8 +1,10 @@
import { SqlEditor } from 'icons'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { useAppStateSnapshot } from 'state/app-state'
import { Button } from 'ui'
import { SqlEditor } from 'icons'
const InlineEditorButton = () => {
const { closeAssistant } = useAiAssistantStateSnapshot()
const { setEditorPanel, editorPanel } = useAppStateSnapshot()
return (
@@ -12,6 +14,7 @@ const InlineEditorButton = () => {
id="editor-trigger"
className="h-full w-full rounded-none text-foreground-light"
onClick={() => {
closeAssistant()
setEditorPanel({ open: !editorPanel.open })
}}
>
@@ -237,10 +237,11 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
'w-screen h-[100dvh] md:h-auto md:w-auto'
)}
>
{aiSnap.open && (
{aiSnap.open ? (
<AIAssistant className="w-full h-[100dvh] md:h-full max-h-[100dvh]" />
)}
{editorPanel.open && <EditorPanel />}
) : editorPanel.open ? (
<EditorPanel />
) : null}
</ResizablePanel>
</>
)}
@@ -48,14 +48,32 @@ import { Message } from './Message'
import { useAutoScroll } from './hooks'
const MemoizedMessage = memo(
({ message, isLoading }: { message: MessageType; isLoading: boolean }) => {
({
message,
isLoading,
onResults,
}: {
message: MessageType
isLoading: boolean
onResults: ({
messageId,
resultId,
results,
}: {
messageId: string
resultId?: string
results: any[]
}) => void
}) => {
return (
<Message
key={message.id}
id={message.id}
role={message.role}
content={message.content}
readOnly={message.role === 'user'}
isLoading={isLoading}
onResults={onResults}
/>
)
}
@@ -134,6 +152,9 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
}
}, [])
// TODO(refactor): This useChat hook should be moved down into each chat session.
// That way we won't have to disable switching chats while the chat is loading,
// and don't run the risk of messages getting mixed up between chats.
const {
messages: chatMessages,
isLoading: isChatLoading,
@@ -160,6 +181,21 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
const canUpdateOrganization = useCheckPermissions(PermissionAction.UPDATE, 'organizations')
const { mutate: updateOrganization, isLoading: isUpdating } = useOrganizationUpdateMutation()
const updateMessage = useCallback(
({
messageId,
resultId,
results,
}: {
messageId: string
resultId?: string
results: any[]
}) => {
snap.updateMessage({ id: messageId, resultId, results })
},
[snap]
)
const renderedMessages = useMemo(
() =>
chatMessages.map((message) => {
@@ -168,6 +204,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
key={message.id}
message={message}
isLoading={isChatLoading && message.id === chatMessages[chatMessages.length - 1].id}
onResults={updateMessage}
/>
)
}),
@@ -294,7 +331,7 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
<TooltipContent side="bottom">Current chat: {currentChat}</TooltipContent>
</Tooltip>
<div className="flex items-center gap-x-2">
<AIAssistantChatSelector />
<AIAssistantChatSelector disabled={isChatLoading} />
<ButtonTooltip
type="default"
size="tiny"
@@ -2,6 +2,7 @@ export type SupportedAssistantEntities = 'rls-policies' | 'functions'
export type SupportedAssistantQuickPromptTypes = 'suggest' | 'examples' | 'ask'
export interface AssistantSnippetProps {
id?: string
title?: string
isChart?: string
runQuery?: string
@@ -22,9 +22,13 @@ import { ButtonTooltip } from '../ButtonTooltip'
interface AIAssistantChatSelectorProps {
className?: string
disabled?: boolean
}
export const AIAssistantChatSelector = ({ className }: AIAssistantChatSelectorProps) => {
export const AIAssistantChatSelector = ({
className,
disabled = false,
}: AIAssistantChatSelectorProps) => {
const snap = useAiAssistantStateSnapshot()
const [chatSelectorOpen, setChatSelectorOpen] = useState(false)
@@ -102,13 +106,15 @@ export const AIAssistantChatSelector = ({ className }: AIAssistantChatSelectorPr
<CommandEmpty_Shadcn_>No chats found.</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
<ScrollArea className={chats.length > 4 ? 'h-40' : ''}>
{/* @ts-ignore */}
{chats.map(([id, chat]) => (
<CommandItem_Shadcn_
key={id}
value={id}
onSelect={() => handleSelectChat(id)}
className="flex items-center justify-between gap-2 py-1 w-full overflow-hidden group"
keywords={[chat.name]}
keywords={!!chat.name ? [chat.name] : undefined}
disabled={disabled}
>
<div className="flex items-center w-full flex-1 min-w-0">
{editingChatId === id ? (
@@ -200,6 +206,7 @@ export const AIAssistantChatSelector = ({ className }: AIAssistantChatSelectorPr
snap.newChat()
setChatSelectorOpen(false)
}}
disabled={disabled}
>
<Plus size={14} strokeWidth={1.5} />
<span>New chat</span>
@@ -19,19 +19,29 @@ const baseMarkdownComponents: Partial<Components> = {
h3: Heading3,
code: InlineCode,
a: Link,
pre: MarkdownPre,
}
interface MessageProps {
id: string
role: 'function' | 'system' | 'user' | 'assistant' | 'data' | 'tool'
content?: string
isLoading: boolean
readOnly?: boolean
action?: React.ReactNode
variant?: 'default' | 'warning'
onResults: ({
messageId,
resultId,
results,
}: {
messageId: string
resultId?: string
results: any[]
}) => void
}
export const Message = function Message({
id,
role,
content,
isLoading,
@@ -39,10 +49,19 @@ export const Message = function Message({
children,
action = null,
variant = 'default',
onResults,
}: PropsWithChildren<MessageProps>) {
const isUser = role === 'user'
const allMarkdownComponents = useMemo(
() => ({ ...markdownComponents, ...baseMarkdownComponents }),
const allMarkdownComponents: Partial<Components> = useMemo(
() => ({
...markdownComponents,
...baseMarkdownComponents,
pre: ({ children }) => (
<MarkdownPre id={id} onResults={onResults}>
{children}
</MarkdownPre>
),
}),
[]
)
@@ -1,5 +1,14 @@
import { useRouter } from 'next/router'
import { DragEvent, memo, ReactNode, useContext, useEffect, useMemo, useRef } from 'react'
import {
DragEvent,
memo,
ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
} from 'react'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { ChartConfig } from 'components/interfaces/SQLEditor/UtilityPanel/ChartConfig'
@@ -7,15 +16,16 @@ import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useFlag } from 'hooks/ui/useFlag'
import { useProfile } from 'lib/profile'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { Dashboards } from 'types'
import { Badge, cn, CodeBlock, CodeBlockLang } from 'ui'
import { DebouncedComponent } from '../DebouncedComponent'
import { EdgeFunctionBlock } from '../EdgeFunctionBlock/EdgeFunctionBlock'
import { QueryBlock } from '../QueryBlock/QueryBlock'
import { AssistantSnippetProps } from './AIAssistant.types'
import { identifyQueryType } from './AIAssistant.utils'
import CollapsibleCodeBlock from './CollapsibleCodeBlock'
import { MessageContext } from './Message'
import { EdgeFunctionBlock } from '../EdgeFunctionBlock/EdgeFunctionBlock'
export const OrderedList = memo(({ children }: { children: ReactNode }) => (
<ol className="flex flex-col gap-y-4">{children}</ol>
@@ -61,7 +71,9 @@ const MemoizedQueryBlock = memo(
isLoading,
isDraggable,
runQuery,
results,
onRunQuery,
onResults,
onDragStart,
onUpdateChartConfig,
}: {
@@ -73,7 +85,9 @@ const MemoizedQueryBlock = memo(
isLoading: boolean
isDraggable: boolean
runQuery: boolean
results?: any[]
onRunQuery: (queryType: 'select' | 'mutation') => void
onResults: (results: any[]) => void
onDragStart: (e: DragEvent<Element>) => void
onUpdateChartConfig?: ({
chart,
@@ -84,7 +98,7 @@ const MemoizedQueryBlock = memo(
}) => void
}) => (
<DebouncedComponent
delay={500}
delay={isLoading ? 500 : 0}
value={sql}
fallback={
<div className="bg-surface-100 border-overlay rounded border shadow-sm px-3 py-2 text-xs">
@@ -118,7 +132,9 @@ const MemoizedQueryBlock = memo(
isLoading={isLoading}
draggable={isDraggable}
runQuery={runQuery}
results={results}
onRunQuery={onRunQuery}
onResults={onResults}
onDragStart={onDragStart}
onUpdateChartConfig={onUpdateChartConfig}
/>
@@ -127,11 +143,28 @@ const MemoizedQueryBlock = memo(
)
MemoizedQueryBlock.displayName = 'MemoizedQueryBlock'
export const MarkdownPre = ({ children }: { children: any }) => {
export const MarkdownPre = ({
children,
id,
onResults,
}: {
children: any
id: string
onResults: ({
messageId,
resultId,
results,
}: {
messageId: string
resultId?: string
results: any[]
}) => void
}) => {
const router = useRouter()
const { profile } = useProfile()
const { isLoading, readOnly } = useContext(MessageContext)
const { mutate: sendEvent } = useSendEventMutation()
const snap = useAiAssistantStateSnapshot()
const supportSQLBlocks = useFlag('reportsV2')
const canCreateSQLSnippet = useCheckPermissions(PermissionAction.CREATE, 'user_content', {
@@ -158,9 +191,11 @@ export const MarkdownPre = ({ children }: { children: any }) => {
)
const { xAxis, yAxis } = snippetProps
const snippetId = snippetProps.id
const title = snippetProps.title || (language === 'edge' ? 'Edge Function' : 'SQL Query')
const isChart = snippetProps.isChart === 'true'
const runQuery = snippetProps.runQuery === 'true'
const results = snap.getCachedSQLResults({ messageId: id, snippetId })
// Strip props from the content for both SQL and edge functions
const cleanContent = rawContent.replace(/(?:--|\/\/)\s*props:\s*\{[^}]+\}/, '').trim()
@@ -178,6 +213,13 @@ export const MarkdownPre = ({ children }: { children: any }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [snippetProps])
const onResultsReturned = useCallback(
(results: any[]) => {
onResults({ messageId: id, resultId: snippetProps.id, results })
},
[onResults, snippetProps.id]
)
const onRunQuery = async (queryType: 'select' | 'mutation') => {
sendEvent({
action: 'assistant_suggestion_run_query_clicked',
@@ -211,8 +253,10 @@ export const MarkdownPre = ({ children }: { children: any }) => {
isChart={isChart}
isLoading={isLoading}
isDraggable={isDraggableToReports}
runQuery={runQuery}
runQuery={!results && runQuery}
results={results}
onRunQuery={onRunQuery}
onResults={onResultsReturned}
onUpdateChartConfig={({ chartConfig: config }) => {
chartConfig.current = { ...chartConfig.current, ...config }
}}
@@ -2,11 +2,15 @@ import { Edit } from 'lucide-react'
import { useRouter } from 'next/router'
import { useParams } from 'common'
import { useIsInlineEditorEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { DiffType } from 'components/interfaces/SQLEditor/SQLEditor.types'
import useNewQuery from 'components/interfaces/SQLEditor/hooks'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import Link from 'next/link'
import { ComponentProps } from 'react'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { useAppStateSnapshot } from 'state/app-state'
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
import {
cn,
@@ -17,9 +21,6 @@ import {
TooltipContent,
} from 'ui'
import { ButtonTooltip } from '../ButtonTooltip'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useAppStateSnapshot } from 'state/app-state'
import { useIsInlineEditorEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
interface EditQueryButtonProps {
id?: string
@@ -39,8 +40,11 @@ export const EditQueryButton = ({
const router = useRouter()
const { ref } = useParams()
const { newQuery } = useNewQuery()
const sqlEditorSnap = useSqlEditorV2StateSnapshot()
const { setEditorPanel } = useAppStateSnapshot()
const snap = useAiAssistantStateSnapshot()
const isInSQLEditor = router.pathname.includes('/sql')
const isInNewSnippet = router.pathname.endsWith('/sql')
const isInlineEditorEnabled = useIsInlineEditorEnabled()
@@ -51,16 +55,6 @@ export const EditQueryButton = ({
const org = useSelectedOrganization()
const { mutate: sendEvent } = useSendEventMutation()
const handleEditInSQLEditor = () => {
if (sql) {
if (isInSQLEditor) {
sqlEditorSnap.setDiffContent(sql, DiffType.Addition)
} else {
newQuery(sql, title)
}
}
}
if (id !== undefined) {
return (
<ButtonTooltip
@@ -88,8 +82,9 @@ export const EditQueryButton = ({
open: true,
initialValue: sql,
})
snap.closeAssistant()
} else {
handleEditInSQLEditor()
if (sql) newQuery(sql, title)
}
sendEvent({
action: 'assistant_edit_in_sql_editor_clicked',
@@ -124,7 +119,7 @@ export const EditQueryButton = ({
>
Replace code
</DropdownMenuItem>
<DropdownMenuItem onClick={() => sqlEditorSnap.setDiffContent(sql, DiffType.NewSnippet)}>
<DropdownMenuItem onClick={() => newQuery(sql, title)}>
Create new snippet
</DropdownMenuItem>
</DropdownMenuContent>
@@ -11,24 +11,15 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte
import { useExecuteSqlMutation } from 'data/sql/execute-sql-mutation'
import { Parameter, parseParameters } from 'lib/sql-parameters'
import { Dashboards } from 'types'
import {
Button,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
cn,
CodeBlock,
SQL_ICON,
} from 'ui'
import { Admonition } from 'ui-patterns'
import { ChartContainer, ChartTooltip, ChartTooltipContent, cn, CodeBlock, SQL_ICON } from 'ui'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import { containsUnknownFunction, isReadOnlySelect } from '../AIAssistantPanel/AIAssistant.utils'
import { ButtonTooltip } from '../ButtonTooltip'
import SqlWarningAdmonition from '../SqlWarningAdmonition'
import { BlockViewConfiguration } from './BlockViewConfiguration'
import { EditQueryButton } from './EditQueryButton'
import { ParametersPopover } from './ParametersPopover'
import { getCumulativeResults } from './QueryBlock.utils'
import SqlWarningAdmonition from '../SqlWarningAdmonition'
export const DEFAULT_CHART_CONFIG: ChartConfig = {
type: 'bar',
@@ -69,12 +60,16 @@ interface QueryBlockProps {
draggable?: boolean
/** Tooltip when hovering over the header of the block (Used in Assistant Panel) */
tooltip?: ReactNode
/** Optional: Any initial results to render as part of the query*/
results?: any[]
/** Not implemented yet: Will be the next part of ReportsV2 */
onSetParameter?: (params: Parameter[]) => void
/** Optional callback the SQL query is run */
onRunQuery?: (queryType: 'select' | 'mutation') => void
/** Optional callback on drag start */
onDragStart?: (e: DragEvent<Element>) => void
/** Optional: callback when the results are returned from running the SQL query*/
onResults?: (results: any[]) => void
// [Joshen] Params below are currently only used by ReportsV2 (Might revisit to see how to improve these)
/** Optional height set to render the SQL query (Used in Reports) */
@@ -116,10 +111,12 @@ export const QueryBlock = ({
disableRunIfMutation = false,
noResultPlaceholder = null,
tooltip,
results,
onRunQuery,
onSetParameter,
onUpdateChartConfig,
onDragStart,
onResults,
}: QueryBlockProps) => {
const { ref } = useParams()
const { project } = useProjectContext()
@@ -128,7 +125,7 @@ export const QueryBlock = ({
const { xKey, yKey, view = 'table' } = chartSettings
const [showSql, setShowSql] = useState(_showSql)
const [queryResult, setQueryResult] = useState<any[]>()
const [queryResult, setQueryResult] = useState<any[] | undefined>(results)
const [parameterValues, setParameterValues] = useState<Record<string, string>>({})
const [showWarning, setShowWarning] = useState<'hasWriteOperation' | 'hasUnknownFunctions'>()
@@ -141,7 +138,10 @@ export const QueryBlock = ({
const isReadOnlySelectSQL = isReadOnlySelect(sql ?? '')
const { mutate: execute, isLoading: isExecuting } = useExecuteSqlMutation({
onSuccess: (data) => setQueryResult(data.result),
onSuccess: (data) => {
onResults?.(data.result)
setQueryResult(data.result)
},
})
const handleExecute = () => {
+2 -1
View File
@@ -69,9 +69,10 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) {
- Always use semicolons
- Output as markdown
- Always include code snippets if available
- If a code snippet is SQL, the first line of the snippet should always be -- props: {"title": "Query title", "runQuery": "false", "isChart": "true", "xAxis": "columnOrAlias", "yAxis": "columnOrAlias"}
- If a code snippet is SQL, the first line of the snippet should always be -- props: {"id": "id", "title": "Query title", "runQuery": "false", "isChart": "true", "xAxis": "columnOrAlias", "yAxis": "columnOrAlias"}
- Only include one line of comment props per markdown snippet, even if the snippet has multiple queries
- Only set chart to true if the query makes sense as a chart. xAxis and yAxis need to be columns or aliases returned by the query.
- Set the id to a random uuidv4 value
- Only set runQuery to true if the query has no risk of writing data and is not a debugging request. Set it to false if there are any values that need to be replaced with real data.
- Explain what the snippet does in a sentence or two before showing it
- Use vector(384) data type for any embedding/vector related query
+35 -1
View File
@@ -9,10 +9,12 @@ type SuggestionsType = {
prompts?: string[]
}
type AssistantMessageType = MessageType & { results?: { [id: string]: any[] } }
type ChatSession = {
id: string
name: string
messages: readonly MessageType[]
messages: readonly AssistantMessageType[]
createdAt: Date
updatedAt: Date
}
@@ -195,6 +197,29 @@ export const createAiAssistantState = (projectRef: string | undefined) => {
}
},
updateMessage: ({
id,
resultId,
results,
}: {
id: string
resultId?: string
results: any[]
}) => {
let chat = state.activeChat
if (!chat || !resultId) return
const existingMessages = chat.messages
const updatedMessages = existingMessages.map((msg) => {
if (msg.id === id) {
return { ...msg, results: { ...(msg.results ?? {}), [resultId]: results } }
} else {
return msg
}
})
chat.messages = updatedMessages
},
setSqlSnippets: (snippets: string[]) => {
state.sqlSnippets = snippets
},
@@ -205,6 +230,15 @@ export const createAiAssistantState = (projectRef: string | undefined) => {
// Remove suggestions if sqlSnippets were removed
state.suggestions = undefined
},
getCachedSQLResults: ({ messageId, snippetId }: { messageId: string; snippetId?: string }) => {
let chat = state.activeChat
if (!chat || !snippetId) return
const message = chat.messages.find((msg) => msg.id === messageId)
const results = (message?.results ?? {})[snippetId]
return results
},
})
// If there's no active chat when the state is created
@@ -43,11 +43,7 @@ The File Upload component makes it easy to add file uploads to your app, with bu
```tsx
'use client'
import {
Dropzone,
DropzoneContent,
DropzoneEmptyState,
} from '@/components/dropzone'
import { Dropzone, DropzoneContent, DropzoneEmptyState } from '@/components/dropzone'
import { useSupabaseUpload } from '@/hooks/use-supabase-upload'
const FileUploadDemo = () => {
@@ -69,7 +65,7 @@ const FileUploadDemo = () => {
)
}
export FileUploadDemo
export { FileUploadDemo }
```
## Props
@@ -41,11 +41,7 @@ The File Upload component makes it easy to add file uploads to your app, with bu
- For control over file upload, you can pass in a `props` object to the component.
```tsx
import {
Dropzone,
DropzoneContent,
DropzoneEmptyState,
} from '@/components/dropzone'
import { Dropzone, DropzoneContent, DropzoneEmptyState } from '@/components/dropzone'
import { useSupabaseUpload } from '@/hooks/use-supabase-upload'
const FileUploadDemo = () => {
@@ -67,7 +63,7 @@ const FileUploadDemo = () => {
)
}
export FileUploadDemo
export { FileUploadDemo }
```
## Props
@@ -41,11 +41,7 @@ The File Upload component makes it easy to add file uploads to your app, with bu
- For control over file upload, you can pass in a `props` object to the component.
```tsx
import {
Dropzone,
DropzoneContent,
DropzoneEmptyState,
} from '@/components/dropzone'
import { Dropzone, DropzoneContent, DropzoneEmptyState } from '@/components/dropzone'
import { useSupabaseUpload } from '@/hooks/use-supabase-upload'
const FileUploadDemo = () => {
@@ -67,7 +63,7 @@ const FileUploadDemo = () => {
)
}
export FileUploadDemo
export { FileUploadDemo }
```
## Props
@@ -41,11 +41,7 @@ The File Upload component makes it easy to add file uploads to your app, with bu
- For control over file upload, you can pass in a `props` object to the component.
```tsx
import {
Dropzone,
DropzoneContent,
DropzoneEmptyState,
} from '@/components/dropzone'
import { Dropzone, DropzoneContent, DropzoneEmptyState } from '@/components/dropzone'
import { useSupabaseUpload } from '@/hooks/use-supabase-upload'
const FileUploadDemo = () => {
@@ -67,7 +63,7 @@ const FileUploadDemo = () => {
)
}
export FileUploadDemo
export { FileUploadDemo }
```
## Props
@@ -15,7 +15,7 @@ thumb: lw14-clerk-tpa-pricing/clerk-tpa-pricing-thumb.png
launchweek: 14
---
Supabase is a modular platform. You can choose which products you use: one or all. Using Supabase Auth one way to add authentication to your application, but it’s not the only choice. This is why today we're expanding our [third-party auth integrations](https://supabase.com/docs/guides/auth/third-party/overview) to officially include [Clerk](https://clerk.com).
Supabase is a modular platform. You can choose which products you use: one or all. Using Supabase Auth is one way to add authentication to your application, but it’s not the only choice. This is why today we're expanding our [third-party auth integrations](https://supabase.com/docs/guides/auth/third-party/overview) to officially include [Clerk](https://clerk.com).
![Clerk TPA diagram](/images/blog/lw14-clerk-tpa-pricing/clerk-tpa-diagram.png)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB