fix markdown spacing draft

This commit is contained in:
Mathew Pareles 2025-03-04 05:39:42 -08:00
parent 1b77fc7091
commit ead1d9229f
4 changed files with 216 additions and 60 deletions

View file

@ -15,6 +15,9 @@ export type ChatMessageLocation = {
}
const cn = (className: string) => className?.split(' ').map(c => c ? `void-${c}` : '').join(' ')
type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string }
const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => {
@ -23,28 +26,93 @@ const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) =>
export const CodeSpan = ({ children, className }: { children: React.ReactNode, className?: string }) => {
return <code className={`
bg-void-bg-1
px-1
rounded-sm
font-mono font-medium
break-all
${className}
`}
>
{children}
</code>
// all classnames must go in tailwind.config.js/safelist
export const noSpaceStyles = {
blockquote: 'pl-4 border-l-4 border-void-bg-2 italic',
br: '',
code: '',
codespan: 'bg-void-bg-1 px-1 rounded-sm font-mono font-medium break-all',
def: '',
del: 'line-through',
em: 'italic',
escape: '',
heading: {
h1: "text-4xl font-semibold pb-2 border-b border-void-bg-2",
h2: "text-3xl font-semibold pb-2 border-b border-void-bg-2",
h3: "text-2xl font-semibold",
h4: "text-xl font-semibold",
h5: "text-lg font-semibold",
h6: "text-base font-semibold text-gray-600"
},
hr: 'border-t border-void-bg-2',
html: '',
image: 'max-w-full h-auto rounded',
link: 'underline cursor-pointer',
list: 'list-inside pl-2',
list_item: '',
paragraph: '',
space: '',
strong: 'font-semibold',
table: 'overflow-x-auto',
text: '',
}
const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocationForApply?: ChatMessageLocation, tokenIdx: string }): JSX.Element => {
const defaultStyles = {
blockquote: 'mx-2 pl-4 border-l-4 border-void-bg-2 italic my-4',
br: '',
code: 'mx-2 my-4',
codespan: 'bg-void-bg-1 px-1 rounded-sm font-mono font-medium break-all',
def: '',
del: 'line-through',
em: 'italic',
escape: '',
heading: {
h1: 'mx-2 text-4xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2',
h2: 'mx-2 text-3xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2',
h3: 'mx-2 text-2xl font-semibold mt-6 mb-4',
h4: 'mx-2 text-xl font-semibold mt-6 mb-4',
h5: 'mx-2 text-lg font-semibold mt-6 mb-4',
h6: 'mx-2 text-base font-semibold mt-6 mb-4 text-gray-600'
},
hr: 'mx-2 my-6 border-t border-void-bg-2',
html: 'mx-2 my-4',
image: 'mx-2 my-4 max-w-full h-auto rounded',
link: 'mx-2 underline',
list: 'mx-2 my-2 list-inside pl-2',
list_item: 'mx-2 mb-2',
paragraph: 'mx-2 my-4',
space: '',
strong: 'mx-2 font-semibold',
table: 'mx-2 my-4 overflow-x-auto',
text: '',
}
type TokenClasses = typeof defaultStyles
const RenderToken = ({ token, nested, chatMessageLocationForApply, tokenIdx, classes }: { token: Token | string, nested?: boolean, chatMessageLocationForApply?: ChatMessageLocation, tokenIdx: string, classes?: TokenClasses }): JSX.Element => {
// deal with built-in tokens first (assume marked token)
const t = token as MarkedToken
if(t.raw.trim() ===''){
return <></>;
}
// compute the className
const defaultClassName = defaultStyles[t.type]
const classNameOverride = classes?.[t.type]
const _className = classNameOverride ?? defaultClassName
let className: string = ''
if (typeof defaultClassName === 'string') {
className = _className as string
}
if (t.type === "space") {
return <span>{t.raw}</span>
return <span className={cn(className)}>{t.raw}</span>
}
if (t.type === "code") {
@ -55,32 +123,28 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, toke
tokenIdx: tokenIdx,
}) : null
return <div className='my-4'>
return <div className={cn(className)}>
<BlockCode
initValue={t.text}
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]}
buttonsOnHover={applyBoxId && <ApplyBlockHoverButtons applyBoxId={applyBoxId} codeStr={t.text} />}
/>
initValue={t.text}
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]}
buttonsOnHover={applyBoxId && <ApplyBlockHoverButtons applyBoxId={applyBoxId} codeStr={t.text} />}
/>
</div>
}
if (t.type === "heading") {
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements
const headingClasses: { [h: string]: string } = {
h1: "text-4xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2",
h2: "text-3xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2",
h3: "text-2xl font-semibold mt-6 mb-4",
h4: "text-xl font-semibold mt-6 mb-4",
h5: "text-lg font-semibold mt-6 mb-4",
h6: "text-base font-semibold mt-6 mb-4 text-gray-600"
}
return <HeadingTag className={headingClasses[HeadingTag]}>{t.text}</HeadingTag>
const HeadingTag = `h${t.depth}` as keyof typeof defaultStyles.heading
const className = classes?.heading[HeadingTag] ?? defaultStyles.heading[HeadingTag]
return <HeadingTag className={cn(className)}>{t.text}</HeadingTag>
}
if (t.type === "table") {
return (
<div className={`${noSpace ? '' : 'my-4'} overflow-x-auto`}>
<table className="min-w-full border border-void-bg-2">
<div className={cn(className)}>
<table className={"min-w-full border border-void-bg-2"}>
<thead>
<tr className="bg-void-bg-1">
{t.header.map((cell: any, index: number) => (
@ -100,7 +164,7 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, toke
{row.map((cell: any, cellIndex: number) => (
<td
key={cellIndex}
className="px-4 py-2 border border-void-bg-2"
className={"px-4 py-2 border border-void-bg-2"}
style={{ textAlign: t.align[cellIndex] || "left" }}
>
{cell.raw}
@ -115,22 +179,34 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, toke
}
if (t.type === "hr") {
return <hr className="my-6 border-t border-void-bg-2" />
return <hr className={cn(className)} />
}
if (t.type === "blockquote") {
return <blockquote className={`pl-4 border-l-4 border-void-bg-2 italic ${noSpace ? '' : 'my-4'}`}>{t.text}</blockquote>
return <blockquote className={cn(className)}>{t.text}</blockquote>
}
if (t.type === 'list_item') {
<li className={cn(className)}>
<span className="ml-1">
!!!!!!!!!!!!!
<ChatMarkdownRender chatMessageLocationForApply={chatMessageLocationForApply} string={t.text} nested={true} />
</span>
</li>
}
if (t.type === "list") {
const ListTag = t.ordered ? "ol" : "ul"
const itemClassName = classes?.['list_item'] ?? defaultStyles['list_item']
return (
<ListTag
start={t.start ? t.start : undefined}
className={`list-inside pl-2 ${noSpace ? '' : 'my-4'} ${t.ordered ? "list-decimal" : "list-disc"}`}
className={`${cn(className)} ${t.ordered ? "list-decimal" : "list-disc"}`}
>
{t.items.map((item, index) => (
<li key={index} className={`${noSpace ? '' : 'mb-4'}`}>
<li key={index} className={cn(itemClassName)}>
{item.task && (
<input type="checkbox" checked={item.checked} readOnly className="mr-2 form-checkbox" />
)}
@ -145,10 +221,10 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, toke
// return (
// <ListTag
// start={t.start ? t.start : undefined}
// className={`pl-2 ${noSpace ? '' : 'my-4'} ${t.ordered ? "list-decimal" : "list-disc"}`}
// className={`${className} ${t.ordered ? "list-decimal" : "list-disc"}`}
// >
// {t.items.map((item, index) => (
// <li key={index} className={`${noSpace ? '' : 'mb-2'} ml-4`}>
// <li key={index} className={`itemClassName`}>
// {item.task && (
// <input type="checkbox" className='mr-2 form-checkbox' checked={item.checked} readOnly />
// )}
@ -164,26 +240,26 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, toke
if (t.type === "paragraph") {
const contents = <>
{t.tokens.map((token, index) => (
<RenderToken key={index} token={token} tokenIdx={`${tokenIdx ? `${tokenIdx}-` : ''}${index}`} /> // assign a unique tokenId to nested components
<RenderToken key={index} token={token} tokenIdx={`${tokenIdx ? `${tokenIdx}-` : ''}${index}`} classes={classes} /> // assign a unique tokenId to nested components
))}
</>
if (nested) return contents
return <p className={`${noSpace ? '' : 'my-4'}`}>
return <p className={cn(className)}>
{contents}
</p>
}
if (t.type === "html") {
return (
<p className={`${noSpace ? '' : 'my-4'}`}>
<p className={cn(className)}>
{t.raw}
</p>
)
}
if (t.type === "text" || t.type === "escape") {
return <span>{t.raw}</span>
return <span className={cn(className)}>{t.raw}</span>
}
if (t.type === "def") {
@ -193,7 +269,7 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, toke
if (t.type === "link") {
return (
<a
className='underline'
className={cn(className)}
onClick={() => { window.open(t.href) }}
href={t.href}
title={t.title ?? undefined}
@ -208,51 +284,52 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, toke
src={t.href}
alt={t.text}
title={t.title ?? undefined}
className={`max4w-full h-auto rounded ${noSpace ? '' : 'my-4'}`}
className={cn(className)}
/>
}
if (t.type === "strong") {
return <strong className="font-semibold">{t.text}</strong>
return <strong className={cn(className)}>{t.text}</strong>
}
if (t.type === "em") {
return <em className="italic">{t.text}</em>
return <em className={cn(className)}>{t.text}</em>
}
// inline code
if (t.type === "codespan") {
return (
<CodeSpan>
<code className={cn(className)}>
{t.text}
</CodeSpan>
</code>
)
}
if (t.type === "br") {
return <br />
return <br className={cn(className)} />
}
// strikethrough
if (t.type === "del") {
return <del className="line-through">{t.text}</del>
return <del className={cn(className)}>{t.text}</del>
}
// default
return (
<div className="bg-orange-50 rounded-sm overflow-hidden p-2">
<span className="text-sm text-orange-500">Unknown type:</span>
{t.type}
{t.raw}
</div>
)
}
export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatMessageLocationForApply }: { string: string, nested?: boolean, noSpace?: boolean, chatMessageLocationForApply?: ChatMessageLocation }) => {
export const ChatMarkdownRender = ({ string, nested = false, classes, chatMessageLocationForApply }: { string: string, nested?: boolean, classes?: TokenClasses, chatMessageLocationForApply?: ChatMessageLocation }) => {
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
return (
<>
{tokens.map((token, index) => (
<RenderToken key={index} token={token} nested={nested} noSpace={noSpace} chatMessageLocationForApply={chatMessageLocationForApply} tokenIdx={index + ''} />
<RenderToken key={index} token={token} nested={nested} classes={classes} chatMessageLocationForApply={chatMessageLocationForApply} tokenIdx={index + ''} />
))}
</>
)

View file

@ -1,3 +1,6 @@
//!!!! merged
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.

View file

@ -15,7 +15,7 @@ import { isWindows, isLinux, isMacintosh } from '../../../../../../../base/commo
import { URI } from '../../../../../../../base/common/uri.js'
import { env } from '../../../../../../../base/common/process.js'
import { ModelDropdown } from './ModelDropdown.js'
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
import { ChatMarkdownRender, noSpaceStyles } from '../markdown/ChatMarkdownRender.js'
import { WarningBox } from './WarningBox.js'
import { os } from '../../../../common/helpers/systemInfo.js'
@ -293,7 +293,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
isPasswordField={isPasswordField}
/>
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-sm'>
<ChatMarkdownRender noSpace string={subTextMd} />
<ChatMarkdownRender classes={noSpaceStyles} string={subTextMd} />
</div>}
</div>
@ -413,11 +413,11 @@ export const FeaturesTab = () => {
{/* <h3 className={`mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3> */}
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
<div className='pl-4 opacity-50'>
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`1. Download [Ollama](https://ollama.com/download).`} /></span>
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`2. Open your terminal.`} /></span>
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender noSpace string={`3. Run \`ollama run llama3.1:8b\`. This installs Meta's llama3.1 model which is best for chat and inline edits. Requires 5GB of memory.`} /></span>
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender noSpace string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This installs a faster autocomplete model. Requires 1GB of memory.`} /></span>
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`Void automatically detects locally running models and enables them.`} /></span>
<span className={`text-sm mb-2`}><ChatMarkdownRender classes={noSpaceStyles} string={`1. Download [Ollama](https://ollama.com/download).`} /></span>
<span className={`text-sm mb-2`}><ChatMarkdownRender classes={noSpaceStyles} string={`2. Open your terminal.`} /></span>
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender classes={noSpaceStyles} string={`3. Run \`ollama run llama3.1:8b\`. This installs Meta's llama3.1 model which is best for chat and inline edits. Requires 5GB of memory.`} /></span>
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender classes={noSpaceStyles} string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This installs a faster autocomplete model. Requires 1GB of memory.`} /></span>
<span className={`text-sm mb-2`}><ChatMarkdownRender classes={noSpaceStyles} string={`Void automatically detects locally running models and enables them.`} /></span>
{/* TODO we should create UI for downloading models without user going into terminal */}
</div>

View file

@ -167,6 +167,82 @@ module.exports = {
},
},
plugins: [],
prefix: 'void-'
prefix: 'void-',
safelist: [
// Background colors
'void-bg-void-bg-1',
// Borders
'void-border-b',
'void-border-l-4',
'void-border-t',
'void-border-void-bg-2',
// Typography
'void-text-2xl',
'void-text-3xl',
'void-text-4xl',
'void-text-base',
'void-text-lg',
'void-text-xl',
'void-text-gray-600',
'void-font-medium',
'void-font-mono',
'void-font-semibold',
'void-italic',
'void-line-through',
'void-underline',
// Spacing
'void-mt-1',
'void-mt-2',
'void-mt-4',
'void-mt-6',
'void-mb-1',
'void-mb-2',
'void-mb-4',
'void-mx-1',
'void-mx-2',
'void-mx-4',
'void-my-1',
'void-my-2',
'void-my-4',
'void-my-6',
'void-pb-1',
'void-pb-2',
'void-pb-4',
'void-pl-1',
'void-pl-2',
'void-pl-4',
'void-px-1',
'void-px-2',
'void-px-4',
// Sizing and layout
'void-h-auto',
'void-max-w-full',
'void-overflow-x-auto',
// Lists
'void-list-inside',
'void-list-decimal',
'void-list-disc',
// Effects and decoration
'void-cursor-pointer',
'void-ring-8',
'void-ring-[#123456]',
'void-rounded',
'void-rounded-sm',
// misc
'void-break-all',
'void-bg-void-bg-1',
'void-px-1',
'void-rounded-sm',
'void-font-mono',
'void-font-medium',
'void-break-all'
]
}