-
Notifications
You must be signed in to change notification settings - Fork 81
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Annotated Text component #462
Changes from 1 commit
374d3fc
2d4ae72
29de0f2
924a1f9
671547a
249e9a0
4b8962f
5a3cb5d
916015b
79a4276
ede0cf7
0bfddf9
633b748
7d19c42
4acd3d3
0f4e1cb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,339 @@ | ||||||||||||||
<template> | ||||||||||||||
<div class="AnnotatedText"> | ||||||||||||||
<span v-for="(content, i) in fields.text.value" :key="content + i"> | ||||||||||||||
<template v-if="typeof content === 'string'">{{ | ||||||||||||||
content | ||||||||||||||
}}</template> | ||||||||||||||
<span | ||||||||||||||
v-if="Array.isArray(content)" | ||||||||||||||
class="annotation" | ||||||||||||||
:style="{ background: content[2] || generateColor(content[1]) }" | ||||||||||||||
> | ||||||||||||||
{{ content[0] }} | ||||||||||||||
<span v-if="content[1]" class="annotation-subject">{{ | ||||||||||||||
getShortName(content[1]) | ||||||||||||||
}}</span> | ||||||||||||||
</span> | ||||||||||||||
</span> | ||||||||||||||
<template v-if="fields.copyButtons.value === 'yes'"> | ||||||||||||||
<div class="controls"> | ||||||||||||||
<button | ||||||||||||||
class="control-button" | ||||||||||||||
@click="copyJSON(fields.text.value)" | ||||||||||||||
> | ||||||||||||||
Copy JSON | ||||||||||||||
</button> | ||||||||||||||
<button | ||||||||||||||
class="control-button" | ||||||||||||||
@click="copyText(fields.text.value)" | ||||||||||||||
> | ||||||||||||||
Copy raw | ||||||||||||||
</button> | ||||||||||||||
</div> | ||||||||||||||
</template> | ||||||||||||||
</div> | ||||||||||||||
</template> | ||||||||||||||
|
||||||||||||||
<script lang="ts"> | ||||||||||||||
import { cssClasses, primaryTextColor } from "../renderer/sharedStyleFields"; | ||||||||||||||
export default { | ||||||||||||||
writer: { | ||||||||||||||
name: "Annotated text", | ||||||||||||||
description: "Shows text with annotations", | ||||||||||||||
category: "Content", | ||||||||||||||
|
||||||||||||||
fields: { | ||||||||||||||
text: { | ||||||||||||||
name: "KeyValue", | ||||||||||||||
type: FieldType.Object, | ||||||||||||||
desc: "Value array with text/annotations. Must be a JSON string or a state reference to an array.", | ||||||||||||||
default: `["This ",["is", "Verb"]," some ",["annotated", "Adjective"], ["text", "Noun"]," for those of ",["you", "Pronoun"]," who ",["like", "Verb"]," this sort of ",["thing", "Noun"],". ","And here's a ",["word", "", "#faf"]," with a fancy background but no label."]`, | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's go for |
||||||||||||||
}, | ||||||||||||||
referenceColor: { | ||||||||||||||
name: "Reference", | ||||||||||||||
desc: "The colour to be used as reference for chroma and luminance, and as the starting point for hue rotation.", | ||||||||||||||
type: FieldType.Color, | ||||||||||||||
default: "#5551FF", | ||||||||||||||
category: FieldCategory.Style, | ||||||||||||||
}, | ||||||||||||||
seed: { | ||||||||||||||
name: "Seed value", | ||||||||||||||
desc: "Choose a different value to reshuffle colours.", | ||||||||||||||
type: FieldType.Number, | ||||||||||||||
default: "1", | ||||||||||||||
category: FieldCategory.Style, | ||||||||||||||
}, | ||||||||||||||
rotateHue: { | ||||||||||||||
name: "Rotate hue", | ||||||||||||||
desc: "If active, rotates the hue depending on the content of the string. If turned off, the reference colour is always used.", | ||||||||||||||
type: FieldType.Text, | ||||||||||||||
options: { | ||||||||||||||
yes: "yes", | ||||||||||||||
no: "no", | ||||||||||||||
}, | ||||||||||||||
default: "no", | ||||||||||||||
category: FieldCategory.Style, | ||||||||||||||
}, | ||||||||||||||
copyButtons: { | ||||||||||||||
name: "Copy buttons", | ||||||||||||||
desc: "If active, adds a control bar with both copy text and JSON buttons.", | ||||||||||||||
type: FieldType.Text, | ||||||||||||||
options: { | ||||||||||||||
yes: "yes", | ||||||||||||||
no: "no", | ||||||||||||||
}, | ||||||||||||||
default: "no", | ||||||||||||||
category: FieldCategory.Style, | ||||||||||||||
}, | ||||||||||||||
primaryTextColor, | ||||||||||||||
cssClasses, | ||||||||||||||
}, | ||||||||||||||
previewField: "text", | ||||||||||||||
}, | ||||||||||||||
}; | ||||||||||||||
</script> | ||||||||||||||
<script setup lang="ts"> | ||||||||||||||
import { FieldCategory, FieldType } from "../writerTypes"; | ||||||||||||||
import injectionKeys from "../injectionKeys"; | ||||||||||||||
import { inject } from "vue"; | ||||||||||||||
import chroma, { Color } from "chroma-js"; | ||||||||||||||
|
||||||||||||||
const fields = inject(injectionKeys.evaluatedFields); | ||||||||||||||
|
||||||||||||||
const COLOR_STEPS = [ | ||||||||||||||
{ h: -78, s: -34, l: 16 }, | ||||||||||||||
{ h: -61, s: -37, l: 16 }, | ||||||||||||||
{ h: -2, s: 0, l: 24 }, | ||||||||||||||
{ h: -12, s: 0, l: 29 }, | ||||||||||||||
{ h: 28, s: -20, l: 24 }, | ||||||||||||||
{ h: -61, s: -95, l: 25 }, | ||||||||||||||
{ h: -173, s: 0, l: 16 }, | ||||||||||||||
{ h: -228, s: 0, l: 22 }, | ||||||||||||||
{ h: 69, s: 0, l: 25 }, | ||||||||||||||
{ h: 70, s: 0, l: 29 }, | ||||||||||||||
]; | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Think about how to extend to support more colors, maybe consider discussing this with Miffy. Right now, I'm using the Tag colors as specified in the design system. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ramedina86 I've asked Miffy a while ago on the additional colors and she pointed to the other color groups. The problem is that they don't match 😅 I was waiting for Pete being in the office to ambush him but he keeps avoiding me so far. It seems like it's either we're going with 10 colors or an LLM is out best bet 🤣 |
||||||||||||||
|
||||||||||||||
let currentSteps = [...COLOR_STEPS]; | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You might wonder why am I doing that. For the tags it's perfectly fine to have the same color for for different tags. Here it's critical to not repeat the colors. More below. |
||||||||||||||
let subjectColorCache = {}; | ||||||||||||||
let lastSeed = fields.seed.value; | ||||||||||||||
Comment on lines
+108
to
+110
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you sure you don't need to make them reactive ?
Suggested change
If its not refs, Vue.js can't track them and might not re-render on update (doc) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It doesn't seem that we need reactivity in this case. I am following There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, let's not use reactivity that we don't need. |
||||||||||||||
|
||||||||||||||
function generateColorCss( | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Feel free about moving to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @polymorpheuz We may choose to have a file for color transformations, since they're similar across tags and annotated text |
||||||||||||||
baseColor: Color, | ||||||||||||||
colorData: { h: number; s: number; l: number }, | ||||||||||||||
) { | ||||||||||||||
let genColor = baseColor | ||||||||||||||
.set( | ||||||||||||||
"hsl.h", | ||||||||||||||
`${Math.sign(colorData.h) == -1 ? "-" : "+"}${Math.abs(colorData.h)}`, | ||||||||||||||
) | ||||||||||||||
.set( | ||||||||||||||
"hsl.s", | ||||||||||||||
`${Math.sign(colorData.s) == -1 ? "-" : "+"}${Math.abs(colorData.s / 100.0)}`, | ||||||||||||||
) | ||||||||||||||
.set( | ||||||||||||||
"hsl.l", | ||||||||||||||
`${Math.sign(colorData.l) == -1 ? "-" : "+"}${Math.abs(colorData.l / 100.0)}`, | ||||||||||||||
); | ||||||||||||||
|
||||||||||||||
return genColor.css(); | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
function calculateColorStep(s: string, stepsLength = COLOR_STEPS.length) { | ||||||||||||||
let hash = (fields.seed.value * 52673) & 0xffffffff; | ||||||||||||||
for (let i = 0; i < s.length; i++) { | ||||||||||||||
hash = ((i + 1) * s.charCodeAt(i)) ^ (hash & 0xffffffff); | ||||||||||||||
} | ||||||||||||||
const step = Math.abs(hash) % stepsLength; | ||||||||||||||
|
||||||||||||||
return step; | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
function generateColor(s: string) { | ||||||||||||||
if (fields.rotateHue.value == "no") { | ||||||||||||||
return fields.referenceColor.value; | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
const baseColor = chroma(fields.referenceColor.value); | ||||||||||||||
|
||||||||||||||
if (lastSeed !== fields.seed.value) { | ||||||||||||||
currentSteps = [...COLOR_STEPS]; | ||||||||||||||
subjectColorCache = {}; | ||||||||||||||
lastSeed = fields.seed.value; | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
if (subjectColorCache[s]) { | ||||||||||||||
return generateColorCss(baseColor, subjectColorCache[s]); | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
// If we run out of colors, reset the list | ||||||||||||||
if (currentSteps.length === 0) { | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a safeguard in order to keep it operational even if we ran out of colors. We shouldn't allow that though and it's better to add like +10-20 color to what we have now. |
||||||||||||||
currentSteps = [...COLOR_STEPS]; | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
const colorStep = calculateColorStep(s, currentSteps.length); | ||||||||||||||
const colorData = currentSteps[colorStep]; | ||||||||||||||
|
||||||||||||||
subjectColorCache[s] = colorData; | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So we make sure to cache the initial allocation here |
||||||||||||||
currentSteps.splice(colorStep, 1); | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ..and we get rid of that color so the random pick won't set us up to have the same color for another subject |
||||||||||||||
|
||||||||||||||
return generateColorCss(baseColor, colorData); | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
function copyText(arr: string[]) { | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's move copy functionality and the bar to |
||||||||||||||
const text = arr.reduce((acc, val) => { | ||||||||||||||
if (typeof val === "string") { | ||||||||||||||
return acc + val; | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
return acc + val[0]; | ||||||||||||||
}, ""); | ||||||||||||||
|
||||||||||||||
copyToClipboard({ text }); | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
function copyJSON(arr: string[]) { | ||||||||||||||
try { | ||||||||||||||
copyToClipboard({ text: JSON.stringify(arr) }); | ||||||||||||||
} catch (e) { | ||||||||||||||
copyToClipboard({ text: arr.join("") }); | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
function setClipboardData<T = unknown>( | ||||||||||||||
source: T & { clipboardData: DataTransfer | null | undefined }, | ||||||||||||||
{ text, html }: { text?: string; html?: string }, | ||||||||||||||
): void { | ||||||||||||||
if (text) { | ||||||||||||||
source.clipboardData?.setData("text/plain", text); | ||||||||||||||
source.clipboardData?.setData("Text", text); // IE mimetype | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
if (html) { | ||||||||||||||
source.clipboardData?.setData("text/html", html); | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
function copyToClipboard({ | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe these two copy functions should travel someplace else but I have no understanding where to. |
||||||||||||||
text = "", | ||||||||||||||
html = "", | ||||||||||||||
}: { | ||||||||||||||
text?: string; | ||||||||||||||
html?: string; | ||||||||||||||
}): boolean { | ||||||||||||||
if ( | ||||||||||||||
(window as any)?.clipboardData && | ||||||||||||||
(window as any)?.clipboardData.setData | ||||||||||||||
) { | ||||||||||||||
// Internet Explorer-specific code path to prevent textarea being shown while dialog is visible. | ||||||||||||||
setClipboardData<any>(window, { text, html }); | ||||||||||||||
|
||||||||||||||
return true; | ||||||||||||||
} else if ( | ||||||||||||||
document.queryCommandSupported && | ||||||||||||||
document.queryCommandSupported("copy") | ||||||||||||||
) { | ||||||||||||||
const copyListener = (event: ClipboardEvent) => { | ||||||||||||||
event.preventDefault(); | ||||||||||||||
setClipboardData<ClipboardEvent>(event, { text, html }); | ||||||||||||||
}; | ||||||||||||||
|
||||||||||||||
document.addEventListener("copy", copyListener, false); | ||||||||||||||
|
||||||||||||||
const textarea = document.createElement("textarea"); | ||||||||||||||
textarea.textContent = text || html; | ||||||||||||||
textarea.style.position = "fixed"; | ||||||||||||||
document.body.appendChild(textarea); | ||||||||||||||
textarea.select(); | ||||||||||||||
|
||||||||||||||
try { | ||||||||||||||
return document.execCommand("copy"); // Security exception may be thrown by some browsers. | ||||||||||||||
} catch (ex) { | ||||||||||||||
return false; | ||||||||||||||
} finally { | ||||||||||||||
document.body.removeChild(textarea); | ||||||||||||||
document.removeEventListener("copy", copyListener, false); | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
return false; | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
const defaultShortNameDictionary = { | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's remove this dict, people can enter the whole thing |
||||||||||||||
verb: "Verb", | ||||||||||||||
adjective: "Adj", | ||||||||||||||
noun: "Noun", | ||||||||||||||
pronoun: "Pronoun", | ||||||||||||||
}; | ||||||||||||||
|
||||||||||||||
function getShortName(name: string) { | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if we even need it. In our reference package they do things like that. |
||||||||||||||
if (!name) { | ||||||||||||||
return ""; | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
const lowerCaseName = name.toLowerCase(); | ||||||||||||||
if (defaultShortNameDictionary[lowerCaseName]) { | ||||||||||||||
return `${defaultShortNameDictionary[lowerCaseName]}`; | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
return name[0].toUpperCase() + name.slice(1); | ||||||||||||||
} | ||||||||||||||
</script> | ||||||||||||||
|
||||||||||||||
<style scoped> | ||||||||||||||
.AnnotatedText { | ||||||||||||||
color: var(--primaryTextColor); | ||||||||||||||
line-height: 1.8; | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
.annotation { | ||||||||||||||
flex-direction: row; | ||||||||||||||
align-items: center; | ||||||||||||||
background: rgba(255, 164, 33, 0.4); | ||||||||||||||
border-radius: 0.5rem; | ||||||||||||||
padding: 0.08rem 0.5rem; | ||||||||||||||
overflow: hidden; | ||||||||||||||
line-height: 1; | ||||||||||||||
vertical-align: middle; | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
.annotation-subject { | ||||||||||||||
display: inline-flex; | ||||||||||||||
font-size: 0.6rem; | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's use px instead of rem I use rem for font-sizes, but looking to change to px too |
||||||||||||||
margin-left: 1rem; | ||||||||||||||
opacity: 0.5; | ||||||||||||||
position: relative; | ||||||||||||||
vertical-align: middle; | ||||||||||||||
|
||||||||||||||
&::after { | ||||||||||||||
content: ""; | ||||||||||||||
border-left: 1px solid; | ||||||||||||||
opacity: 0.1; | ||||||||||||||
position: absolute; | ||||||||||||||
top: 0px; | ||||||||||||||
left: -0.55rem; | ||||||||||||||
height: 10px; | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
.controls { | ||||||||||||||
margin: 10px 0; | ||||||||||||||
display: flex; | ||||||||||||||
flex-direction: row; | ||||||||||||||
justify-content: flex-end; | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
.control-button { | ||||||||||||||
background-color: var(--buttonColor); | ||||||||||||||
border: none; | ||||||||||||||
border-radius: 0.5rem; | ||||||||||||||
color: white; | ||||||||||||||
cursor: pointer; | ||||||||||||||
font-size: 0.7rem; | ||||||||||||||
margin-right: 10px; | ||||||||||||||
padding: 0.25rem 0.5rem; | ||||||||||||||
|
||||||||||||||
&:hover { | ||||||||||||||
opacity: 0.9; | ||||||||||||||
} | ||||||||||||||
} | ||||||||||||||
</style> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
content
can be a number and then you can have duplicated key. I suggest to cast it to be safe