Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90250ca076 | ||
| 50a2a37d6b | |||
|
|
7c97b79956 | ||
| 1be9e76012 |
6 changed files with 85 additions and 8 deletions
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"defaultHomeserver": 0,
|
||||
"homeserverList": [
|
||||
"chat.gamecult.org",
|
||||
"gamecult.org",
|
||||
"converser.eu",
|
||||
"matrix.org",
|
||||
|
|
|
|||
40
src/app/components/message/pose/sentiment.test.ts
Normal file
40
src/app/components/message/pose/sentiment.test.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { classifyPose, messagePose } from './sentiment';
|
||||
|
||||
describe('classifyPose', () => {
|
||||
it.each([
|
||||
['annoyed', 'ugh, this again'],
|
||||
['surprised', 'wow, no way'],
|
||||
['amused', 'hahaha that is funny'],
|
||||
['happy', 'thanks, great work'],
|
||||
['thinking', 'should we ship it?'],
|
||||
['neutral', 'Merged the branch.'],
|
||||
['neutral', 'sorry, that failed with an error'],
|
||||
])('%s', (label, text) => expect(classifyPose(text)).toBe(label));
|
||||
it('is neutral for empty or non-string text', () => {
|
||||
expect(classifyPose('')).toBe('neutral');
|
||||
expect(classifyPose(undefined)).toBe('neutral');
|
||||
expect(classifyPose(5)).toBe('neutral');
|
||||
});
|
||||
it('is deterministic', () => {
|
||||
expect(classifyPose('thanks?')).toBe(classifyPose('thanks?'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('messagePose', () => {
|
||||
it('lets an explicit label win over the text', () => {
|
||||
expect(messagePose('aqua', { body: 'thanks!', 'org.gamecult.pose': 'annoyed' })).toBe(
|
||||
'annoyed'
|
||||
);
|
||||
});
|
||||
it('keeps an unknown explicit label neutral instead of classifying', () => {
|
||||
expect(messagePose('aqua', { body: 'thanks!', 'org.gamecult.pose': 'conduct' })).toBe(
|
||||
'neutral'
|
||||
);
|
||||
});
|
||||
it('classifies when there is no label, and never outputs a signature label', () => {
|
||||
expect(messagePose('aqua', { body: 'thanks!' })).toBe('happy');
|
||||
expect(messagePose('kiko', { body: 'sorry' })).toBe('neutral');
|
||||
expect(messagePose('aqua', {})).toBe('neutral');
|
||||
});
|
||||
});
|
||||
35
src/app/components/message/pose/sentiment.ts
Normal file
35
src/app/components/message/pose/sentiment.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { POSE_KEY, NEUTRAL, resolvePose } from './poses';
|
||||
|
||||
// catalogue.json beside this file is a vendored copy of /opt/commons/cast-art/poses/catalogue.json
|
||||
// (npm run sync:poses); nothing else publishes it to the browser.
|
||||
// Local sentiment -> pose label. A lexicon over the message text, no network, no model: the
|
||||
// same text always gives the same pose. Rules are tried in order and the first hit wins, so a
|
||||
// message that says "wow" and "thanks" reads as surprised. Only the shared labels (identical art
|
||||
// for all eight Minds, catalogue.json) are ever output; signature labels are explicit-only.
|
||||
const RULES: [string, RegExp][] = [
|
||||
['annoyed', /\b(annoy\w*|ugh+|argh+|frustrat\w*|again\?|why won'?t|stop it)\b/i],
|
||||
['surprised', /\b(wow|whoa|woah|omg|no way|unexpected\w*|surpris\w*|wait,? what)\b|[?!]{2,}/i],
|
||||
[
|
||||
'determined',
|
||||
/\b(let'?s (go|do|fix|ship)|on it|i'?ll (fix|do|handle)|will do|fixing|resolved?)\b/i,
|
||||
],
|
||||
['amused', /\b(ha(ha)+|lol|lmao|rofl|funny|hilarious)\b|😂|🤣/i],
|
||||
[
|
||||
'happy',
|
||||
/\b(thanks?|thank you|thx|great|awesome|nice|love|perfect|excellent|yay|congrats)\b|🎉|😊/i,
|
||||
],
|
||||
['thinking', /\?\s*$|\b(hmm+|maybe|perhaps|i wonder|not sure|how (do|does|about)|what if)\b/i],
|
||||
];
|
||||
|
||||
export const classifyPose = (text: unknown): string => {
|
||||
if (typeof text !== 'string') return NEUTRAL;
|
||||
const hit = RULES.find(([, re]) => re.test(text));
|
||||
return hit ? hit[0] : NEUTRAL;
|
||||
};
|
||||
|
||||
// An explicit org.gamecult.pose label always wins, even when it is unknown (then neutral).
|
||||
// Only events with no label are read for sentiment.
|
||||
export const messagePose = (mind: string, content: Record<string, unknown>): string =>
|
||||
content[POSE_KEY] !== undefined
|
||||
? resolvePose(mind, content[POSE_KEY])
|
||||
: resolvePose(mind, classifyPose(content.body));
|
||||
|
|
@ -61,7 +61,8 @@ import {
|
|||
import { MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { PoseAvatar } from '../../../components/message/pose/PoseAvatar';
|
||||
import { POSE_KEY, MINDS, mindOfSender, resolvePose } from '../../../components/message/pose/poses';
|
||||
import { MINDS, mindOfSender } from '../../../components/message/pose/poses';
|
||||
import { messagePose } from '../../../components/message/pose/sentiment';
|
||||
import { useEntranceDelay } from '../../../components/message/pose/entrance';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
|
||||
|
|
@ -835,7 +836,7 @@ export const Message = as<'div', MessageProps>(
|
|||
const poseJSX = !collapse && mind && (
|
||||
<PoseAvatar
|
||||
mind={mind}
|
||||
pose={resolvePose(mind, mEvent.getContent()[POSE_KEY])}
|
||||
pose={messagePose(mind, mEvent.getContent())}
|
||||
userId={senderId}
|
||||
alt={poseAlt}
|
||||
enter={enterDelay !== undefined}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import { ClientConfig, clientDefaultServer } from './useClientConfig';
|
|||
import config from '../../../config.json';
|
||||
|
||||
describe('default homeserver', () => {
|
||||
it('is gamecult.org when the config names none', () => {
|
||||
expect(clientDefaultServer({})).toBe('gamecult.org');
|
||||
expect(clientDefaultServer({ homeserverList: [] })).toBe('gamecult.org');
|
||||
it('is chat.gamecult.org when the config names none', () => {
|
||||
expect(clientDefaultServer({})).toBe('chat.gamecult.org');
|
||||
expect(clientDefaultServer({ homeserverList: [] })).toBe('chat.gamecult.org');
|
||||
});
|
||||
|
||||
it('is the listed server the config points at', () => {
|
||||
|
|
@ -14,7 +14,7 @@ describe('default homeserver', () => {
|
|||
).toBe('chat.gamecult.org');
|
||||
});
|
||||
|
||||
it('is gamecult.org in the repo config.json too', () => {
|
||||
expect(clientDefaultServer(config as ClientConfig)).toBe('gamecult.org');
|
||||
it('is chat.gamecult.org in the repo config.json too', () => {
|
||||
expect(clientDefaultServer(config as ClientConfig)).toBe('chat.gamecult.org');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export function useClientConfig(): ClientConfig {
|
|||
}
|
||||
|
||||
export const clientDefaultServer = (clientConfig: ClientConfig): string =>
|
||||
clientConfig.homeserverList?.[clientConfig.defaultHomeserver ?? 0] ?? 'gamecult.org';
|
||||
clientConfig.homeserverList?.[clientConfig.defaultHomeserver ?? 0] ?? 'chat.gamecult.org';
|
||||
|
||||
export const clientAllowedServer = (clientConfig: ClientConfig, server: string): boolean => {
|
||||
const { homeserverList, allowCustomHomeservers } = clientConfig;
|
||||
|
|
|
|||
Loading…
Reference in a new issue