add CTA people mapping from murmurations
This commit is contained in:
parent
df4707ff7b
commit
cc1c044dcf
|
|
@ -1,16 +0,0 @@
|
|||
from terminusdb_client import WOQLClient
|
||||
import json
|
||||
|
||||
# we keep all the information in dictionaries with Employee id as keys
|
||||
orgs = {}
|
||||
|
||||
client = WOQLClient("https://cloud.terminusdb.com/Myseelia/")
|
||||
client.connect(db="playground", team="Myseelia", use_token=True)
|
||||
|
||||
with open("../src/components/explore/knowledge_graph.json") as json_file:
|
||||
data = json.load(json_file)
|
||||
entities = data.get("entities")
|
||||
for entity in entities:
|
||||
if entity.get("type") == "organization":
|
||||
id = entity.get("id")
|
||||
client.deleteDocument(id);
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"branch": "main", "ref": null}
|
||||
Binary file not shown.
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"database": "murmurations",
|
||||
"endpoint": "https://cloud.terminusdb.com/Myseelia/",
|
||||
"team": "Myseelia",
|
||||
"use JWT token": true
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
from terminusdb_client import WOQLClient
|
||||
|
||||
client = WOQLClient("https://cloud.terminusdb.com/Myseelia/")
|
||||
client.connect(db="murmurations", team="Myseelia", use_token=True)
|
||||
|
||||
# Query for all documents with type "person"
|
||||
query = client.query_document({"@type": "person"})
|
||||
ids = []
|
||||
# Delete each document by its ID
|
||||
for doc in list(query):
|
||||
ids.append(doc['@id'])
|
||||
client.delete_document(ids)
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
import csv
|
||||
from itertools import islice
|
||||
from schema import person
|
||||
from terminusdb_client import WOQLClient
|
||||
from datetime import datetime
|
||||
import pytz
|
||||
import re
|
||||
import json
|
||||
import meilisearch
|
||||
import ast
|
||||
import hashlib
|
||||
import requests
|
||||
import emoji
|
||||
|
||||
def get_emoji_regexp():
|
||||
# Sort emoji by length to make sure multi-character emojis are
|
||||
# matched first
|
||||
emojis = sorted(emoji.EMOJI_DATA, key=len, reverse=True)
|
||||
pattern = u'(' + u'|'.join(re.escape(u) for u in emojis) + u')'
|
||||
return re.compile(pattern)
|
||||
|
||||
|
||||
def remove_emojis(string):
|
||||
return get_emoji_regexp().sub(r'', string)
|
||||
|
||||
# we keep all the information in dictionaries with Employee id as keys
|
||||
orgs = {}
|
||||
orgsjson = []
|
||||
|
||||
client = WOQLClient("https://cloud.terminusdb.com/Myseelia/")
|
||||
client.connect(db="murmurations", team="Myseelia", use_token=True)
|
||||
|
||||
client1 = meilisearch.Client(
|
||||
'https://ms-9ea4a96f02a8-1969.sfo.meilisearch.io', '117c691a34b21a6651798479ebffd181eb276958')
|
||||
|
||||
def delete_index(index_name):
|
||||
try:
|
||||
index = client1.index(index_name)
|
||||
response = index.delete()
|
||||
print(response)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
delete_index('people')
|
||||
|
||||
index = client1.index('people')
|
||||
|
||||
# Define the endpoint and headers for the API request
|
||||
endpoint = 'https://test-index.murmurations.network/v2/nodes'
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
# # Load the input data from a file
|
||||
# with open('murmuration_people.json', 'r') as f:
|
||||
# input_data = json.load(f)
|
||||
|
||||
# Load the input data from a URL
|
||||
url = "https://test-index.murmurations.network/v2/nodes?schema=person_schema-v0.1.0"
|
||||
input_data = requests.get(url).json()
|
||||
|
||||
# Extract the data field from the input data
|
||||
response_data = input_data['data']
|
||||
|
||||
# Create a dictionary to keep track of the profile URLs that have already been processed
|
||||
profile_urls = {}
|
||||
|
||||
# Create a dictionary to keep track of people based on their profile URL
|
||||
people_dict = {}
|
||||
|
||||
# Create a list to store the people as `person` objects
|
||||
people = []
|
||||
|
||||
for profile in response_data:
|
||||
# Define the data to be sent in the GET request
|
||||
endpoint = profile['profile_url']
|
||||
headers = {'accept': 'application/json'}
|
||||
|
||||
# Send the GET request to retrieve the profile details
|
||||
response = requests.get(endpoint, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
# Parse the JSON data and extract the necessary information to create a `person` object
|
||||
json_data = response.json()
|
||||
name = json_data.get('name', None)
|
||||
description = json_data.get('description', None)
|
||||
primary_url = json_data.get('primary_url', None)
|
||||
image = json_data.get('image', None)
|
||||
locality = json_data.get('locality', None)
|
||||
profile_url = json_data.get('profile_url', None)
|
||||
|
||||
# Check if the person is already in the database
|
||||
if profile_url in profile_urls:
|
||||
continue
|
||||
|
||||
personname = remove_emojis(str(name))
|
||||
|
||||
# Create a `Person` object with the extracted information and the people they know
|
||||
newperson = person(
|
||||
name=personname,
|
||||
description=str(description),
|
||||
primary_url=str(primary_url),
|
||||
image=str(image),
|
||||
locality=str(locality),
|
||||
vouches_for=set(),
|
||||
LI=set()
|
||||
)
|
||||
# if personname not blank
|
||||
if personname != 'None':
|
||||
people_dict[personname] = newperson
|
||||
people.append(newperson)
|
||||
profile_urls[primary_url] = endpoint
|
||||
else:
|
||||
print(f"Error {response.status_code}: {response.reason}")
|
||||
|
||||
# Update the temporary person objects with missing information
|
||||
for p in people:
|
||||
if not p.name or not p.description or not p.image or not p.locality or not p.vouches_for or not p.LI:
|
||||
profileurl = profile_urls[p.primary_url]
|
||||
response = requests.get(profileurl, headers={'accept': 'application/json'})
|
||||
if response.status_code == 200:
|
||||
json_data = response.json()
|
||||
p.name = json_data.get('name', None)
|
||||
p.description = json_data.get('description', '')
|
||||
p.image = json_data.get('image', '')
|
||||
p.locality = json_data.get('locality', '')
|
||||
p.vouches_for = set()
|
||||
p.LI = set()
|
||||
for person_data in json_data.get('knows', []):
|
||||
url = person_data.get('url')
|
||||
incommunity = False
|
||||
if url not in profile_urls.values():
|
||||
continue
|
||||
print(person_data.get('name'))
|
||||
knowsname = person_data.get('name')
|
||||
relationship_type = person_data.get('type')
|
||||
if relationship_type == 'VOUCHES_FOR':
|
||||
if knowsname in people_dict:
|
||||
p.vouches_for.add(people_dict[knowsname])
|
||||
elif relationship_type == 'LI':
|
||||
if knowsname in people_dict:
|
||||
p.LI.add(people_dict[knowsname])
|
||||
else:
|
||||
print(f"Error {response.status_code}: {response.reason}")
|
||||
|
||||
|
||||
BATCH_SIZE = 100
|
||||
|
||||
# Split the people list into batches
|
||||
batches = [people[i:i+BATCH_SIZE] for i in range(0, len(people), BATCH_SIZE)]
|
||||
|
||||
# Insert each batch into TerminusDB
|
||||
inserted = []
|
||||
for batch in batches:
|
||||
print(batch)
|
||||
batch_inserted = client.insert_document(batch, commit_msg="Adding people")
|
||||
print("inserted")
|
||||
inserted.extend(batch_inserted)
|
||||
document_ids = [doc_id for doc_id in inserted]
|
||||
|
||||
print("done inserting")
|
||||
|
||||
# Retrieve all documents at once
|
||||
documents = client.query_document({"@type": "person"})
|
||||
|
||||
# Process each document
|
||||
indexed_documents = []
|
||||
for document in documents:
|
||||
real_id = document['@id']
|
||||
num_id = real_id.split("/")[-1]
|
||||
document = {k: json.dumps(v) for k, v in document.items() if k != '@id'}
|
||||
document.update({'id': num_id})
|
||||
indexed_documents.append(document)
|
||||
|
||||
# Add all indexed documents to the index at once
|
||||
index.add_documents(indexed_documents)
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import csv
|
||||
from itertools import islice
|
||||
from schema import person
|
||||
from terminusdb_client import WOQLClient
|
||||
from datetime import datetime
|
||||
import pytz
|
||||
import re
|
||||
import json
|
||||
import meilisearch
|
||||
import ast
|
||||
import hashlib
|
||||
import requests
|
||||
|
||||
# we keep all the information in dictionaries with Employee id as keys
|
||||
orgs = {}
|
||||
orgsjson = []
|
||||
|
||||
client = WOQLClient("https://cloud.terminusdb.com/Myseelia/")
|
||||
client.connect(db="murmurations", team="Myseelia", use_token=True)
|
||||
|
||||
client1 = meilisearch.Client(
|
||||
'https://ms-9ea4a96f02a8-1969.sfo.meilisearch.io', '117c691a34b21a6651798479ebffd181eb276958')
|
||||
|
||||
index = client1.index('people')
|
||||
|
||||
# Define the endpoint and headers for the API request
|
||||
endpoint = 'https://test-index.murmurations.network/v2/nodes'
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
# Load the input data from a file
|
||||
with open('murmuration_people.json', 'r') as f:
|
||||
input_data = json.load(f)
|
||||
|
||||
# Extract the data field from the input data
|
||||
response_data = input_data['data']
|
||||
|
||||
# Loop through the response data and retrieve each profile's details with a curl request
|
||||
people = []
|
||||
person1 = person(
|
||||
name="alice",
|
||||
primary_url="https://test-index.murmurations.network",
|
||||
)
|
||||
person2 = person(
|
||||
name="alice",
|
||||
primary_url="https://test-index.murmurations.network",
|
||||
)
|
||||
person3 = person(
|
||||
name="bob",
|
||||
primary_url="https://test-index.murmurations.network3",
|
||||
LI={person1}
|
||||
)
|
||||
person4 = person(
|
||||
name="steve",
|
||||
primary_url="https://test-index.murmurations.network4",
|
||||
LI={person2}
|
||||
)
|
||||
|
||||
people.append(person1)
|
||||
people.append(person2)
|
||||
people.append(person3)
|
||||
people.append(person4)
|
||||
inserted = client.insert_document(people, commit_msg="Adding people")
|
||||
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"data": [
|
||||
{
|
||||
"last_updated": 1677532593,
|
||||
"linked_schemas": ["person_schema-v0.1.0"],
|
||||
"locality": "Kortenaken",
|
||||
"primary_url": "noo.network/user/33ed23b1-b6af-4d2b-96bf-ac050645fc97",
|
||||
"profile_url": "https://noo.network/api/murmur/network/33ed23b1-b6af-4d2b-96bf-ac050645fc97",
|
||||
"status": "posted"
|
||||
},
|
||||
{
|
||||
"last_updated": 1676554597,
|
||||
"linked_schemas": ["person_schema-v0.1.0"],
|
||||
"primary_url": "noo.network/user/5793fa86-79ab-491c-a662-b66fe784c8dc",
|
||||
"profile_url": "https://noo.network/api/murmur/network/5793fa86-79ab-491c-a662-b66fe784c8dc",
|
||||
"status": "posted"
|
||||
},
|
||||
{
|
||||
"last_updated": 1677874496,
|
||||
"linked_schemas": ["person_schema-v0.1.0"],
|
||||
"locality": "Victoria, BC",
|
||||
"primary_url": "noo.network/user/5d614114-bcc6-4b56-8f62-2a59685668ef",
|
||||
"profile_url": "https://noo.network/api/murmur/network/5d614114-bcc6-4b56-8f62-2a59685668ef",
|
||||
"status": "posted"
|
||||
},
|
||||
{
|
||||
"last_updated": 1676556053,
|
||||
"linked_schemas": ["person_schema-v0.1.0"],
|
||||
"locality": "Oakland, CA",
|
||||
"primary_url": "noo.network/user/646d7156-f985-4d1b-be61-317ad5a843dd",
|
||||
"profile_url": "https://noo.network/api/murmur/network/646d7156-f985-4d1b-be61-317ad5a843dd",
|
||||
"status": "posted"
|
||||
},
|
||||
{
|
||||
"last_updated": 1676555982,
|
||||
"linked_schemas": ["person_schema-v0.1.0"],
|
||||
"locality": "Sunnyvale, CA",
|
||||
"primary_url": "noo.network/user/b7b5897d-fca7-4d06-bfba-63994479be0e",
|
||||
"profile_url": "https://noo.network/api/murmur/network/b7b5897d-fca7-4d06-bfba-63994479be0e",
|
||||
"status": "posted"
|
||||
},
|
||||
{
|
||||
"last_updated": 1677532664,
|
||||
"linked_schemas": ["person_schema-v0.1.0"],
|
||||
"locality": "Zurich, Switzerland",
|
||||
"primary_url": "noo.network/user/e3ad9e2c-40ec-4a89-afc8-b7fec35af6cf",
|
||||
"profile_url": "https://noo.network/api/murmur/network/e3ad9e2c-40ec-4a89-afc8-b7fec35af6cf",
|
||||
"status": "posted"
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"self": "http://test-index.murmurations.network/v2/nodes?schema=person_schema-v0.1.0\u0026page=1"
|
||||
},
|
||||
"meta": { "number_of_results": 6, "total_pages": 1 }
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import requests
|
||||
import json
|
||||
import pandas as pd
|
||||
|
||||
# Define the endpoint and headers for the API request
|
||||
endpoint = 'https://test-index.murmurations.network/v2/nodes'
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
# Load the input data from a file
|
||||
with open('murmuration_people.json', 'r') as f:
|
||||
input_data = json.load(f)
|
||||
|
||||
# Extract the data field from the input data
|
||||
response_data = input_data['data']
|
||||
|
||||
# Loop through the response data and retrieve each profile's details with a curl request
|
||||
profiles = []
|
||||
for profile in response_data:
|
||||
# Define the data to be sent in the GET request
|
||||
endpoint = profile['profile_url']
|
||||
headers = {'accept': 'application/json'}
|
||||
|
||||
# Send the GET request to retrieve the profile details
|
||||
response = requests.get(endpoint, headers=headers)
|
||||
response_data = response.json()
|
||||
print(response_data)
|
||||
profiles.append(response)
|
||||
|
||||
# Convert the profiles list to a Pandas dataframe
|
||||
df = pd.DataFrame(profiles)
|
||||
|
||||
|
||||
# Display the resulting dataframe
|
||||
df.to_csv('output.csv', index=False)
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,39 @@
|
|||
####
|
||||
# This is the script for storing the schema of your TerminusDB
|
||||
# database for your project.
|
||||
# Use 'terminusdb commit' to commit changes to the database and
|
||||
# use 'terminusdb sync' to change this file according to
|
||||
# the exsisting database schema
|
||||
####
|
||||
"""
|
||||
"""
|
||||
from typing import Optional, Set
|
||||
|
||||
from terminusdb_client.woqlschema import (
|
||||
DocumentTemplate,
|
||||
ValueHashKey,
|
||||
)
|
||||
|
||||
|
||||
class person(DocumentTemplate):
|
||||
"""person_schema-v0.1.0
|
||||
|
||||
Attributes
|
||||
----------
|
||||
LI : Set['person']
|
||||
knows
|
||||
vouches_for : Set['person']
|
||||
knows
|
||||
"""
|
||||
|
||||
_key = ValueHashKey()
|
||||
LI: Set["person"]
|
||||
description: Optional[str]
|
||||
image: Optional[str]
|
||||
locality: Optional[str]
|
||||
name: Optional[str]
|
||||
primary_url: Optional[str]
|
||||
vouches_for: Set["person"]
|
||||
|
||||
def __str__(self):
|
||||
return f"person(name={self.name}, description={self.description}, primary_url={self.primary_url}, image={self.image}, locality={self.locality}, vouches_for={self.vouches_for}, LI={self.LI})"
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
<script lang="ts">
|
||||
import cytoscape from 'cytoscape'
|
||||
import { onMount } from 'svelte'
|
||||
import { bubble } from 'svelte/internal'
|
||||
import TerminusClient from '@terminusdb/terminusdb-client'
|
||||
import { MeiliSearch } from 'meilisearch'
|
||||
import { generateKnowledgeGraph } from './cytoscape.ts'
|
||||
|
||||
let cy
|
||||
|
||||
interface INodeData {
|
||||
id: string
|
||||
}
|
||||
|
||||
interface INode {
|
||||
data: INodeData
|
||||
}
|
||||
|
||||
interface IEdgeData {
|
||||
id: string
|
||||
source: string
|
||||
target: string
|
||||
label: string
|
||||
}
|
||||
|
||||
interface IEdge {
|
||||
data: IEdgeData
|
||||
}
|
||||
|
||||
import json_graph from './knowledge_graph.json'
|
||||
|
||||
let knowledgeGraphJson: any = json_graph
|
||||
|
||||
// knowledgeGraphJson = await response.json()
|
||||
// } else {
|
||||
// alert(`HTTP-Error: ${response.status}`)
|
||||
// }
|
||||
// }
|
||||
|
||||
let nodes: INode[] = []
|
||||
let edges: IEdge[] = []
|
||||
|
||||
onMount(async () => {
|
||||
nodes = knowledgeGraphJson.entities.map((entity: any) => ({
|
||||
data: { id: entity.id, label: entity.label }
|
||||
}))
|
||||
|
||||
edges = knowledgeGraphJson.relations.map(
|
||||
(relation: any, index: string) => ({
|
||||
data: {
|
||||
id: index,
|
||||
source: relation.source,
|
||||
target: relation.target,
|
||||
label: relation.type
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
cy = cytoscape({
|
||||
container: document.getElementById('cy'),
|
||||
elements: {
|
||||
nodes,
|
||||
edges
|
||||
},
|
||||
style: [
|
||||
{
|
||||
selector: 'node',
|
||||
style: {
|
||||
'text-valign': 'center',
|
||||
'text-halign': 'center',
|
||||
'text-wrap': 'wrap',
|
||||
'text-max-width': function (ele) {
|
||||
return Math.max(1, Math.ceil(ele.degree() / 2)) * 30
|
||||
},
|
||||
'font-size': function (ele) {
|
||||
return Math.max(1, Math.ceil(ele.degree() / 2)) * 6
|
||||
},
|
||||
'background-color': '#75f6df',
|
||||
'border-color': '#223152',
|
||||
'border-width': function (ele) {
|
||||
return Math.max(1, Math.ceil(ele.degree() / 2))
|
||||
},
|
||||
label: 'data(label)',
|
||||
width: function (ele) {
|
||||
return Math.max(1, Math.ceil(ele.degree() / 2)) * 40
|
||||
},
|
||||
height: function (ele) {
|
||||
return Math.max(1, Math.ceil(ele.degree() / 2)) * 40
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
selector: 'edge',
|
||||
style: {
|
||||
'font-size': 20,
|
||||
width: 5,
|
||||
'line-color': '#223152',
|
||||
'target-arrow-color': '#223152',
|
||||
'target-arrow-shape': 'triangle',
|
||||
'curve-style': 'bezier',
|
||||
'text-rotation': 'autorotate',
|
||||
'text-offset': { x: 20, y: -20 },
|
||||
'text-background-opacity': 1,
|
||||
'text-background-color': '#fafafa',
|
||||
'text-background-shape': 'roundrectangle',
|
||||
label: 'data(label)'
|
||||
}
|
||||
}
|
||||
],
|
||||
layout: {
|
||||
name: 'cose'
|
||||
// infinite: true,
|
||||
}
|
||||
})
|
||||
|
||||
cy.nodes().forEach(function (node) {
|
||||
node.data({
|
||||
degree: node.connectedEdges().length
|
||||
})
|
||||
})
|
||||
|
||||
var nodes = cy.nodes()
|
||||
nodes = nodes.sort(function (a, b) {
|
||||
return b.data('degree') - a.data('degree')
|
||||
})
|
||||
|
||||
var top100 = nodes.slice(0, 1000)
|
||||
|
||||
//console.log(top100)
|
||||
|
||||
cy.nodes().forEach(function (node) {
|
||||
if (!top100.includes(node)) {
|
||||
node.hide()
|
||||
}
|
||||
})
|
||||
|
||||
let toggle = true
|
||||
|
||||
// cy.off('tap', 'node', event => {
|
||||
// const node = event.target;
|
||||
// const nodeId = node.data('id');
|
||||
// alert('unDisplay info for ' + nodeId);
|
||||
// });
|
||||
|
||||
cy.on('tap', 'node', function (evt) {
|
||||
var node = evt.target
|
||||
var connectedEdges = node.connectedEdges()
|
||||
var connectedNodes = node.neighborhood().nodes()
|
||||
var allElements = cy.elements()
|
||||
var allNodes = cy.nodes()
|
||||
var allEdges = cy.edges()
|
||||
|
||||
if (node.style('display') == 'element') {
|
||||
// hide all nodes and edges except the selected node and its neighbors
|
||||
allNodes.style('display', 'none')
|
||||
allEdges.style('display', 'none')
|
||||
connectedNodes.style('display', 'element')
|
||||
node.style('display', 'element')
|
||||
connectedEdges.style('display', 'element')
|
||||
} else {
|
||||
// show all nodes and edges
|
||||
allNodes.style('display', 'element')
|
||||
allEdges.style('display', 'element')
|
||||
}
|
||||
})
|
||||
|
||||
// Reset the state when clicking away from the node
|
||||
cy.on('tap', function (e) {
|
||||
if (e.target === cy) {
|
||||
cy.nodes().style('display', 'element')
|
||||
cy.edges().style('display', 'element')
|
||||
cy.nodes().data('highlighted', false)
|
||||
}
|
||||
})
|
||||
|
||||
cy.on('tap', 'edge', event => {
|
||||
const edge = event.target
|
||||
const edgeId = edge.data('id')
|
||||
alert('Display info for ' + edgeId)
|
||||
})
|
||||
|
||||
// cy.on('tap', 'node', function(){
|
||||
// alert("put code here"));
|
||||
// });
|
||||
|
||||
// cy.layout({
|
||||
// name: 'cola'
|
||||
// }).run();
|
||||
})
|
||||
|
||||
var searchTerm = ''
|
||||
function updateSearchTerm(e) {
|
||||
searchTerm = e.target.value
|
||||
// Perform search in real timebased on searchTerm here
|
||||
}
|
||||
|
||||
async function entered(e) {
|
||||
const searchclient = new MeiliSearch({
|
||||
host: 'https://ms-9ea4a96f02a8-1969.sfo.meilisearch.io',
|
||||
apiKey: '117c691a34b21a6651798479ebffd181eb276958'
|
||||
})
|
||||
const index = searchclient.index('people')
|
||||
// this will search both keys and values
|
||||
// const search = await index.search(e.target.value.toString(), { q: '*' });
|
||||
// const searchResult = await index.search('orgs', {
|
||||
// attributesToRetrieve: ['id']
|
||||
// })
|
||||
const searchResult = await index.search(e.target.value.toString())
|
||||
// need to turn the search results into an array of ids which can be used to query the knowledge graph
|
||||
const resultsgraph = await generateKnowledgeGraph(searchResult.hits).then(
|
||||
resultsgraph => {
|
||||
// console.log(resultsgraph)
|
||||
const allNodes = resultsgraph.entities.map((entity: any) => ({
|
||||
data: { id: entity.id, label: entity.label }
|
||||
}))
|
||||
|
||||
const allEdges = resultsgraph.relations.map(
|
||||
(relation: any, index: string) => ({
|
||||
data: {
|
||||
id: index,
|
||||
source: relation.source,
|
||||
target: relation.target,
|
||||
label: relation.type
|
||||
}
|
||||
})
|
||||
)
|
||||
cy.remove(cy.elements())
|
||||
cy.add(allNodes)
|
||||
cy.add(allEdges)
|
||||
cy.layout({
|
||||
name: 'cose'
|
||||
// other layout options here
|
||||
}).run()
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="pt-8 p-6 md:p-8 mx-auto">
|
||||
<input
|
||||
id="search"
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
on:input={updateSearchTerm}
|
||||
on:keydown={event => {
|
||||
if (event.keyCode === 13) {
|
||||
entered(event)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<section class="overflow-hidden text-gray-700">
|
||||
<div class="cyDiv" />
|
||||
|
||||
<div id="cy" />
|
||||
</section>
|
||||
|
||||
<style>
|
||||
#search {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
z-index: 100;
|
||||
background-color: white;
|
||||
width: 50%;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
padding: 10px 20px 10px 40px;
|
||||
}
|
||||
|
||||
#search input[type='text'] {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 50px;
|
||||
width: 80%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
font-size: 18px;
|
||||
}
|
||||
#cy-div {
|
||||
z-index: 99;
|
||||
}
|
||||
#cy {
|
||||
width: 100%;
|
||||
height: 95%;
|
||||
position: absolute;
|
||||
top: 55px;
|
||||
left: 0px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
// import { isNullOrUndefined } from 'util'
|
||||
import TerminusClient from '@terminusdb/terminusdb-client'
|
||||
|
||||
import * as fs from 'fs'
|
||||
|
||||
const WOQL = TerminusClient.WOQL
|
||||
|
||||
export async function generateKnowledgeGraph(ids: object[]): Promise<object> {
|
||||
console.log(ids)
|
||||
const entities: { id: string; label: string; type: string }[] = []
|
||||
const relations: { source: string; target: string; type: string }[] = []
|
||||
for (const document of ids) {
|
||||
const personid = 'Person/' + document['id']
|
||||
let personEntity = entities.find(entity => entity.id === personid)
|
||||
if (!personEntity) {
|
||||
entities.push({
|
||||
id: personid,
|
||||
label: document['name'],
|
||||
type: 'person'
|
||||
})
|
||||
personEntity = entities[entities.length - 1]
|
||||
}
|
||||
|
||||
const linktypes = ['vouches_for', 'LI']
|
||||
for (const link of linktypes) {
|
||||
let linkValues = document[link]
|
||||
try {
|
||||
linkValues = JSON.parse(linkValues)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
if (Array.isArray(linkValues)) {
|
||||
for (const linkValue of linkValues) {
|
||||
if (linkValue === undefined) continue
|
||||
const linkId = linkValue.replace(/^"|"$/g, '')
|
||||
if (linkId !== undefined && linkId !== '') {
|
||||
let linkEntity = entities.find(
|
||||
entity => entity.id === linkId
|
||||
)
|
||||
if (!linkEntity) {
|
||||
entities.push({
|
||||
id: linkId,
|
||||
label: linkValue,
|
||||
type: 'attribute'
|
||||
})
|
||||
linkEntity = entities[entities.length - 1]
|
||||
}
|
||||
let linkRelation = relations.find(
|
||||
relation =>
|
||||
relation.source === personid && relation.target === linkId
|
||||
)
|
||||
if (!linkRelation) {
|
||||
relations.push({
|
||||
source: personid,
|
||||
target: linkId,
|
||||
type: link
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let linkId = linkValues
|
||||
// console.log(linkId)
|
||||
if (linkId !== undefined && linkId !== '') {
|
||||
linkId = linkId.replace(/^"|"$/g, '')
|
||||
let linkEntity = entities.find(
|
||||
entity => entity.id === linkId
|
||||
)
|
||||
if (!linkEntity) {
|
||||
entities.push({
|
||||
id: linkId,
|
||||
label: document[link],
|
||||
type: 'attribute'
|
||||
})
|
||||
linkEntity = entities[entities.length - 1]
|
||||
}
|
||||
let linkRelation = relations.find(
|
||||
relation =>
|
||||
relation.source === personid && relation.target === linkId
|
||||
)
|
||||
if (!linkRelation) {
|
||||
relations.push({
|
||||
source: personid,
|
||||
target: linkId,
|
||||
type: link
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
entities: entities,
|
||||
relations: relations
|
||||
}
|
||||
}
|
||||
|
||||
export default generateKnowledgeGraph
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
{
|
||||
"entities": [{
|
||||
"label": "Organization",
|
||||
"title": "Neuralink"
|
||||
}, {
|
||||
"label": "Organization",
|
||||
"title": "SpaceX"
|
||||
}, {
|
||||
"label": "Organization",
|
||||
"title": "Pretoria"
|
||||
}, {
|
||||
"label": "Organization",
|
||||
"title": "The Boring Company"
|
||||
}, {
|
||||
"label": "Organization",
|
||||
"title": "University of Pretoria"
|
||||
}, {
|
||||
"label": "Organization",
|
||||
"title": "Stanford University"
|
||||
}, {
|
||||
"label": "Person",
|
||||
"title": "Jeff Bezos"
|
||||
}, {
|
||||
"label": "Organization",
|
||||
"title": "University of Pennsylvania"
|
||||
}, {
|
||||
"label": "Person",
|
||||
"title": "Kimbal Musk"
|
||||
}, {
|
||||
"label": "Organization",
|
||||
"title": "Tesla, Inc."
|
||||
}, {
|
||||
"label": "Person",
|
||||
"title": "Elon Musk"
|
||||
}],
|
||||
"relations": [{
|
||||
"source": "Elon Musk",
|
||||
"target": "Neuralink"
|
||||
}, {
|
||||
"source": "Tesla, Inc.",
|
||||
"target": "Elon Musk",
|
||||
"type": "owned by"
|
||||
}, {
|
||||
"source": "Elon Musk",
|
||||
"target": "University of Pennsylvania",
|
||||
"type": "residence"
|
||||
}, {
|
||||
"source": "Elon Musk",
|
||||
"target": "Tesla, Inc.",
|
||||
"type": "owned by"
|
||||
}, {
|
||||
"source": "The Boring Company",
|
||||
"target": "Tesla, Inc.",
|
||||
"type": "owned by"
|
||||
}, {
|
||||
"source": "Elon Musk",
|
||||
"target": "Kimbal Musk",
|
||||
"type": "sibling"
|
||||
}, {
|
||||
"source": "University of Pennsylvania",
|
||||
"target": "Elon Musk",
|
||||
"type": "residence"
|
||||
}, {
|
||||
"source": "The Boring Company",
|
||||
"target": "Neuralink",
|
||||
"type": "subsidiary"
|
||||
}, {
|
||||
"source": "Elon Musk",
|
||||
"target": "University of Pretoria",
|
||||
"type": "work location"
|
||||
}, {
|
||||
"source": "The Boring Company",
|
||||
"target": "Elon Musk",
|
||||
"type": "owned by"
|
||||
}, {
|
||||
"source": "Kimbal Musk",
|
||||
"target": "Elon Musk",
|
||||
"type": "sibling"
|
||||
}, {
|
||||
"source": "Neuralink",
|
||||
"target": "Elon Musk",
|
||||
"type": "owned by"
|
||||
}, {
|
||||
"source": "Elon Musk",
|
||||
"target": "The Boring Company",
|
||||
"type": "owned by"
|
||||
}, {
|
||||
"source": "Elon Musk",
|
||||
"target": "University of Pennsylvania",
|
||||
"type": "work location"
|
||||
}]
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
Document id,name,assignee,datecreated,description,logo,preJan20thUpvotes,reviewed,submittedbyemail,submittedbyname,submittedbyowner,subscribed,upvotes,impactarea,topic,blockchainecosystem,web3
|
||||
Organization/1e0832fa5474a176e004f0e4d587ee7801bd7272e9abb7743d8127324dc259aa,test,,,,,,,,,,,,,,,
|
||||
Organization/94e4bec6c4397de81b525a97b3d16af681727a5c9f06c7f27cc2564835b59143,tester,,0000-12-31T23:59:59.900Z,,,0,,,,,,0,,,,
|
||||
Organization/9470c06cddf598b90a2e072c71ebe1aff7414f371257809343c92cbb24fdb3ef,darren,,,,,,,,,,,,,,,
|
||||
Organization/04a5b028a7499aa41d5e0e433b5d6128255611becd569bcbda3115700a7c0744,BVRio Environmental Exchange Platform,https://www.bvrio.org/,2022-05-07T11:03:00Z,BVRio's mission is to promote the use of market mechanisms to facilitate compliance with environmental laws and support the green and low carbon economy.,,0,checked,,,,,,['Investing'],['Consulting'],,
|
||||
Organization/0008f7122957abba12e9db800fab7975254bd4ad0cd7c90e93a906566174e936,Sustainable Impact Token,https://sustainableimpacttokens.com/,2022-07-08T10:39:00Z,SIT PRESENTS A FIRST-OF-ITS KIND INVESTMENT OPPORTUNITY UNDERPINNED BY ITS THREE CORE DRIVING OBJECTIVES: 1.Food security 2.Renewable energy 3.Carbon reduction,,,checked,,,,,,['FoodAg'],"['Offsetting', 'Water']",['Tezos'],['Token']
|
||||
Organization/00cc6f5581fa22b43900ac0c0e42fd7ebe3b2e0c9846289fb264b9d3700b9617,Hazel,https://hazelverse.xyz,2022-12-09T11:25:00Z,"Hazel gamifies carbon removal, using NFTs and a casual game mechanic, to bring the public to the carbon-removal market and fight climate change. ",App_Store3x_gu09bjgyh.png (https://v5.airtableusercontent.com/v1/15/15/1675389600000/QApQyC6ogdwCVHtjjqXcOw/dqG9lSZe7D_F-YxLZgamq6hz2DxolRu7dO-AdM19PHR7FD1U6ll7eHzrdOAWsNV8OGPHRYnW2028usrc0c6hGPYYQVL6qjb32_0oae0NLraLYZZAxZ9zPUiqc0QTsXmQ/2bC8jNqUgE7i4PgZxlytnQIsbQ41lagTvu-FnEwy0pI),11,,,,checked,checked,11,['Carbon'],,"['Celo', 'Ethereum', 'Polygon']","['Community', 'Metaverse', 'NFT']"
|
||||
Organization/01403ea7a34e7b764aea71cde72a086a46026596de4101514a7888616a66715b,Open Forest Protocol,https://www.openforestprotocol.org/product,2022-05-07T11:03:00Z,"Transparent, scalable and open-source forest monitoring and carbon financing",open-forest-logo.jpeg (https://v5.airtableusercontent.com/v1/15/15/1675389600000/REle8XxHaWAMQ97Ytj5I3Q/8CaLlOovk58wMA-pzOczx6P1s6_eVUd4eQ1ZI4nFgVTotdc29QQkIIdYAuKdqedfaYDhMvt_NAqIE7YELFT29TLKBlgd2XLwfBfQSTMpWHI/CIjmH4hsWQyrzrTYhUglOwQXSL1EDdr4s7j0PN5pa1Y),9,checked,,,,,9,['Nature'],"['Biodiversity', 'Community']",['Near'],['Token']
|
||||
Organization/014f869845e4f3dddf5d26dfae12c9de8c8be2a6f8e4b5463cd02e2685fe343a,Dream Village,https://dreamvillageghana.org/,2022-10-24T06:51:00Z,"Introducing regenerative agriculture, economies and developmental projects in Ghana",FB_IMG_1666014010988.jpg (https://v5.airtableusercontent.com/v1/15/15/1675389600000/X98CY9sPXQ0YdS-4x_PIgw/aehNZ2rZZiY9jnVjEL9yCWMniFHEefn7IcPBhiuA-5Ze54wbSIES-lO59L2uH__fsuIDdjQJyyQ7masFYd_XQc5twYZ6WmU6aCY5KpE03wY/ylF5DBoSJRoUht6ZC7cc33nPa9LyFRYR0jL4YyvJiaI),,,,,checked,checked,,"['Education', 'FoodAg', 'Innovation', 'Nature']","['Agriculture', 'Animals', 'Biodiversity', 'Energy', 'FoodForests', 'Forestry', 'Water']","['Other', 'RegenNetwork']","['DAO', 'dApp']"
|
||||
Organization/016c0a02a6aa71d885e150f641417b86209a2983d0164d16b7e7446923af87a9,C4EST,cforest.org,2022-10-25T13:26:00Z,"Creating a token-based economy to invest in building plantations near city peripheries most prone to global warming’s effects and turning it into a nature-based bio-economy according to local industrial, social and environmental needs of the cities. ",c4est-logo-white.png (https://v5.airtableusercontent.com/v1/15/15/1675389600000/mEjWQLbUQ_lntUnTIgAwLQ/lY4OM_RR6IXwjLCP2QuR27GHJTTUBLZiy3CxxHbieXR4mvwcCnmUxW6SdXvN2DZkKknk0BYXzAWomfQ4xXkgRzHcOS-1SzOmHGXl4AHml4E/1SGyW1Huo5quCKAZyFMewiNGl2fyYWOuRc9FZNuLKWk),,,,,checked,checked,,"['Investing', 'Carbon', 'Energy', 'FoodAg', 'Industry', 'Infrastructure', 'Nature', 'SocialJustice']","['Agriculture', 'Animals', 'Biodiversity', 'Energy', 'FoodForests', 'Forestry', 'Meteorology', 'Water']",['Other'],"['Blockchain', 'DAO', 'NFT', 'Stablecoin', 'Token']"
|
||||
Organization/019857cb0cfd541aae2988a0f43469d7d854e4821a80de9f2802f95b12d0bd6d,Future Quest,https://future.quest/,2022-10-07T07:27:00Z,"We're building a play-and-earn ecosystem committed to fixing our future. Part game, part launch pad for public good quests for our planet. ",1E755576-AF4D-40C3-ACAF-EB0E7603B88E_eplcl0rwi.jpeg (https://v5.airtableusercontent.com/v1/15/15/1675389600000/8DyTh8VREtSojyYvkgZEfw/sf2GfUZhYh409Pxc-jO9lubI3iBHDyorjvnxSJAE_IL7sOeC3V4JNo74wKB2QwZKAz_50Zwk3M62Ars8QBfEQF8P752SZLTfp20v5QVi2lqAbfepY2lgbqrBUoB9_k7ZborfVygtcu0RwTBbuAGlew/oEaKRPThOtUT5puZQlUqBZ2zg-CHqn31L9wzykPZQps),198,checked,,,,,198,['Investing'],,['Polygon'],['DAO']
|
||||
Organization/01adb0a6eb8da1c4f4ceb2176ad0f43aff28078a02deb1abc68b1346fceef886,KOKO DAO,https://kokodao.xyz/,2022-10-24T06:51:00Z,Proof of impact based on biodiversity and work with local communities,Koko Dao-02.jpg (https://v5.airtableusercontent.com/v1/15/15/1675389600000/OPi-TZ9RwZQ-BR6Bbd71Iw/M3-EE4ZFpK7IFwwYuv7Mze8sIIFZynaZkoJDjazn_GFaTAmA84b49TIx9468HegZfX16S54FswjFj-6joOLSBxD144CF7f8TLjNCqGj-ugI/Dr17MF1t5ySqm1q-uMy8zoSi4_JCXjwjPi-jxc_VbA8),,,,,checked,checked,,"['FoodAg', 'Nature']","['Agriculture', 'Biodiversity', 'FoodForests', 'Forestry']","['Cosmos', 'Other', 'RegenNetwork']",['DAO']
|
||||
Organization/02662d81af586be2cb75939a63e6bc7c9cc5e83cfc623cadf98ab3f28e548bd9,[redacted] labs (name incoming),,2022-10-25T10:39:00Z,DeFi-powered impact financing leveraging the graph to create webs of trust,[r].png (https://v5.airtableusercontent.com/v1/15/15/1675389600000/w9wyV7EUxpyv4EiakYXKrg/itqTMuokAETY6w0rNrPx39nYO_fTv3HPWCWjw67hDAHebcp4Hu8Do9bACW3W-_fPba793iDlUS8ybbWeOOyJcg/A-1il_EVTp_xO3urUmL0KM0QxNT6zSJUjYKEMqliZiw),6,,,,checked,checked,6,"['Investing', 'Carbon', 'Education', 'Energy', 'Infrastructure', 'Innovation', 'Nature', 'SocialJustice']","['Agriculture', 'Biodiversity', 'Energy', 'Forestry']","['Celo', 'Ethereum', 'Other', 'Polygon']","['Blockchain', 'DEX', 'NFT', 'Oracle', 'Other', 'Stablecoin', 'Token', 'dApp']"
|
||||
Organization/02a8610aba2945b5723145dd82e329236c763f703f7e70d0b9725007893d7d35,ORGO,orgo.earth,2023-01-11T13:18:00Z,"ORGO is a Sponsorship Marketplace created to support environmental and social causes, while aiming to close the gap in sustainable finance and rewarding positive behaviors.",ORGO_Logo-04_t44cgzkp6.png (https://v5.airtableusercontent.com/v1/15/15/1675389600000/I1yecpmz-U3zUSYcUDqHbw/dnZTUG352RkLoXhm708w_bS0UzvpA4nYNA8nL3A9Z11hcyKe1RWsEvbFFYWvSxS0AR8RD3-thFSCqTQbxI-HePl1UZ-Qi-Jepn946vhQzxImY4gWRtRJFcELktpiG6bk/VyTIItCojOjg5atTVPjbcE3pM34rHNRpCZr8WTInFbE),,,,,checked,checked,,['Nature'],,"['Polkadot', 'Polygon']","['Community', 'DAO', 'Marketplace', 'NFT', 'Token', 'Wallet', 'dApp']"
|
||||
Organization/031e45922dbae180e6b18df9679087bf8a5c796b2a446416cf95b5389effe3c6,Seatle NFT Museum,https://www.seattlenftmuseum.com/,2022-05-07T11:03:00Z,"Explore the Future of Art. Welcome to Seattle's first NFT art museum, designed to bring together artists, creators, collectors, and the broader blockchain community. ",,,checked,,,,,,['SocialJustice'],"['Art', 'Community', 'Initiative', 'Local']",,['NFT']
|
||||
Organization/0353ac2dd478a36e62deb2dba20307892bb8f25cb4d174c03723cfbc611373ef,Circonomy,https://circon.me/,2022-11-27T15:54:00Z,Our mission and vision is to turn capital goods into public goods by putting the circular economy on-chain and we'll accomplish this with a recycle/reuse-to-earn protocol.,Logo_mvm3cargk.png (https://v5.airtableusercontent.com/v1/15/15/1675389600000/Y269VSJYFyDC5rvpbdYFWQ/GKDZqxVfvDsmJk7VbQdxOioAblOA3InWQepzYmuCLwunOYFZZf3AcoIMe4cGfSTHRClb5X0kbi7OtG09iN6ENARDixI1njKwHvI-O2vfAJ8/yPiaOgnhLaXtGPdDfMulfe96VY_oswaf4ZKPnP9yzRI),,,,,checked,checked,,['Industry'],,"['Celo', 'Near', 'Polygon']","['Blockchain', 'Community', 'DAO', 'Marketplace', 'NFT', 'Token', 'Wallet', 'dApp']"
|
||||
Organization/03a93b37edff42ea223d03209eb8bfb0f3107bfc8e8077fbbaffd452d4ebdc9e,earth.fm,http://earth.fm/,2022-07-08T10:39:00Z,Earth.fm is a non-profit organisation that seeks to protect and regenerate natural ecosystems and reconnect us to the more-than-human world.,,,checked,,,,,,['Nature'],['Biodiversity'],,
|
||||
Organization/03c3c47e38024c1d5b04dfe90e458dbf645478748df406c0f9f07a338b4411bd,The Sun Exchange,https://thesunexchange.com/,2022-05-07T11:03:00Z,"Enter Sun Exchange, the world’s first peer-to-peer solar leasing platform. Through Sun Exchange, anyone, anywhere in the world, can own solar energy-producing cells and build wealth by leasing those cells to power businesses and organisations in emerging markets, with installations and maintenance taken care of by one of Sun Exchange’s carefully selected installation partners. We leverage financial innovation and the power of the crowd to drive sustainable energy development and make the environmental, social and economic benefits of solar accessible and affordable for all.",,,checked,,,,,,['Energy'],"['Fundraising', 'Renewables']",,
|
||||
Organization/03ea9160ae4936db68df53e3d5ad6bf68b581baaa94ed4a41ddc03c6283c48dd,HARA,https://haratoken.io/,2022-05-07T11:03:00Z,Making the invisible visible through the use of technology to accelerate agricultural development for Indonesian farmers to be more equitable.,hara-token.jpeg (https://v5.airtableusercontent.com/v1/15/15/1675389600000/iOVDj9hkXM9q03Bm6j71Cg/VXwxSyxEKrFvTBFTVEQNlp7O0_uJimFkl5dbmWV4Gah8nia3nZL12DxcQcVu7resnlnO-y79JLjD8O3Tk7TupSa6u2lijrR1NwhZVIPUy7I/3pQR2hA-FQA_h6YisCrGCymzBcShanfdcRep_-LY3QY),4,checked,,,,,4,['FoodAg'],"['Community', 'Land', 'Local']",['Ethereum'],"['Blockchain', 'Oracle']"
|
||||
Organization/052479193a721ea298ea93ff64522938f64a1ef4590e6a66bfcf12857e452d9e,"UCO Network
|
||||
",https://www.uco.network/,2022-09-21T15:17:00Z,UCO Network is a public blockchain protocol that is deploying a suite of decentralised technologies that mitigate the risk of fraud and open new and exciting opportunities within the Advanced Bio fuel and Web3. Industries.,,,,,,,,,['Nature'],"['CircularEconomy', 'Traceability', 'waste']",,"['NFT', 'Token']"
|
||||
Organization/053c0a1ae666ac91dd14ee418b079e5585f0d3d79d6cacbaf0f31aec12aa236f,Rebioca,https://www.rebioca.com/,2022-10-24T06:51:00Z,"Rebioca aims to design the products of the future by recycling organic wastes using less water and energy resulting in low carbon emissions, compared to traditional methods and processes.",rebioca-retina.png (https://v5.airtableusercontent.com/v1/15/15/1675389600000/0BuvqJilW-a2JyvVdvw80Q/bySVWUGoI2IFHjRc8JeqqSE5ROhj99AuT7CLlXzz7A5anfwab4LDw8QYYloYwJmJ5SyCfc3SPAMQIRBLRyi6Pubm66NS81OafcAR8XnXMk8/n4Q0fBDszBqBwS4fZRVhcKAIonPtihdvQyPdu2UUPA0),,,,,checked,checked,,"['Carbon', 'Energy', 'FoodAg', 'Nature']","['Agriculture', 'Animals', 'Water']",['Celo'],"['NFT', 'dApp']"
|
||||
Organization/0544651b16d40568dedeaa97f4b8bd9c69a5b5988bbd463cfe081219bba5a132,Pozzle Planet,www.pozzleplanet.com,2022-10-24T06:51:00Z,"Pozzle Planet is a social app is a fun and inspiring way to make a positive impact in your everyday life. Earn POZ by joining and sharing planet positive actions with your friends via short-form video. On the outside, a social app where people generate impact by sharing videos and earning rewards. Underneath the hood, an impact-Protocol and ReFi yield generator that passively farms people’s impact into NFTs called Pozzles that contain impact tokens (Impact2) which represent measurable impact units from the real world. The Pozzle Planet mobile app sits at the intersection of 3 main growth trends in social, p2e gaming and DeFi, uniquely positioned as the first impact-2-earn social platform with tailwinds from increased individual and collective aspirations to be part of planet positive solutions. ",PP App Icon.png (https://v5.airtableusercontent.com/v1/15/15/1675389600000/WpQxVA6gWbRZIEpSBWAU3w/5tLAZtjfE3T-GJS1nifQBwzCSY5hbcjU2Bd8NZ8g3kkk2k9mxlhXRT2yDmXeG4csITwplZmpUctEp4-B1hk4p49bRZ2oTsmGl5T57kKNPTE/ZsQM_kMT8hWKj-FTz8egxkc5dJYjbOUqH78Mj0fNbDo),3,,,,checked,checked,3,"['Investing', 'Carbon', 'Education', 'Nature', 'Other', 'SocialJustice']","['Community', 'Local', 'Other']","['Celo', 'Polygon']","['NFT', 'Token', 'dApp']"
|
||||
Organization/056f40719cfa5cc97d6b969a454032af6e5a92d7f76e8a4fb0b60c41b8fa2814,Global Innovation Lab for Climate Finance (the Lab),http://www.climatefinancelab.org/,2022-05-07T11:03:00Z,"Nations, businesses, and investors are working to move toward a low-carbon, climate resilient economy. Many of the measures underpinning this transition, including energy efficiency, renewable energy, sustainable transport, climate smart agriculture, and curbing deforestation, face specific barriers to attracting investment. By identifying, developing, and supporting transformative sustainable finance ideas, the Lab aims to drive billions of dollars of private investment to the low-carbon economy.",,,checked,,,,,,['Carbon'],"['Fundraising', 'Investing', 'Land', 'Renewables']",,
|
||||
Organization/05bed03da8c15018650d4d7e13d0cc5a1c6c0b990bed5a92cbad818ff1826603,Carbovalent,https://carbovalent.com/,2022-12-22T13:34:00Z,"Carbovalent is a carbon credit network built on the Solana blockchain that aims to address the challenges of transparency, accessibility, liquidity, and composability in traditional off-chain carbon credit registries. ",Picture1_nxlmhoump.png (https://v5.airtableusercontent.com/v1/15/15/1675389600000/NKH1E0WNPC0G2SMM9mki7Q/6R2vCNu-M-CJG05WhhG25yu490EYRYQuGiUowPZW_o6iHP_EJbMQanZgRZNqojQ91RAzKfj14C3YkFyakQX2YvkOignsoU9nFaKUxtNcJ_Y/I4ItZRupaa8rBkbOBQd_hB9pYVB7WAx_GFCoesZ-7EI),911,,,,checked,,911,['Carbon'],,,"['Blockchain', 'Bridge', 'Marketplace', 'NFT', 'Token', 'dApp']"
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
from numpy import NaN
|
||||
from terminusdb_client import WOQLClient
|
||||
from terminusdb_client.woqlschema import WOQLSchema
|
||||
from terminusdb_client.woqldataframe import result_to_df
|
||||
import pandas as pd
|
||||
import json
|
||||
|
||||
# For Terminus X, use the following
|
||||
# client = WOQLClient("https://cloud.terminusdb.com/<Your Team>/")
|
||||
# client.connect(db="demo_workshop", team="<Your Team>", use_token=True)
|
||||
|
||||
client = WOQLClient("https://cloud.terminusdb.com/Myseelia/")
|
||||
client.connect(db="murmurations", team="Myseelia", use_token=True)
|
||||
|
||||
orgs_raw = client.query_document({"@type": "person"})
|
||||
# team_marketing_raw = client.query_document({"@type": "Employee", "team": "marketing"})
|
||||
|
||||
df = result_to_df(orgs_raw)
|
||||
# df_selected = df[0:20]
|
||||
# df = df.head(100)
|
||||
#df.to_csv('df.csv', index=False)
|
||||
entities = []
|
||||
relations = []
|
||||
|
||||
for i, row in df.iterrows():
|
||||
# Create an entity for the person
|
||||
entity = {'id': row['Document id'], 'label': row['name'], 'type': 'person'}
|
||||
# Add any additional properties that exist for the person
|
||||
for prop in ['description', 'image', 'locality', 'primary_url']:
|
||||
if prop in row and not pd.isna(row[prop]):
|
||||
entity[prop] = row[prop]
|
||||
entities.append(entity)
|
||||
|
||||
# Create a relation for each knows relationship
|
||||
if isinstance(row['LI'], list) and len(row['LI']) > 0 and not pd.isna(row['LI'][0]):
|
||||
for j in row['LI']:
|
||||
relation = {'source': row['Document id'], 'target': j, 'type': 'LI'}
|
||||
relations.append(relation)
|
||||
elif not pd.isna(row['LI']):
|
||||
relation = {'source': row['Document id'], 'target': row['LI'], 'type': 'LI'}
|
||||
relations.append(relation)
|
||||
|
||||
# Create a relation for each vouches_for relationship
|
||||
if isinstance(row['vouches_for'], list) and len(row['vouches_for']) > 0 and not pd.isna(row['vouches_for'][0]):
|
||||
for j in row['vouches_for']:
|
||||
relation = {'source': row['Document id'], 'target': j, 'type': 'vouches_for'}
|
||||
relations.append(relation)
|
||||
elif not pd.isna(row['vouches_for']):
|
||||
relation = {'source': row['Document id'], 'target': row['vouches_for'], 'type': 'vouches_for'}
|
||||
relations.append(relation)
|
||||
|
||||
|
||||
knowledgeGraphJson = {
|
||||
'entities': entities,
|
||||
'relations': relations
|
||||
}
|
||||
|
||||
|
||||
with open("knowledge_graph.json", "w") as f:
|
||||
json.dump(knowledgeGraphJson, f)
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -25,6 +25,11 @@
|
|||
href: '/explore/',
|
||||
icon: PhotoGallery
|
||||
},
|
||||
{
|
||||
label: 'CTA',
|
||||
href: '/cta/',
|
||||
icon: PhotoGallery
|
||||
},
|
||||
{
|
||||
label: 'About This Template',
|
||||
href: '/about/',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
<script lang="ts">
|
||||
import Explore from '$components/CTA/CTA.svelte'
|
||||
</script>
|
||||
|
||||
<Explore />
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="66" fill="none">
|
||||
<path
|
||||
stroke="#171717"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M14.167 45.667C7.17 45.667 1.5 39.996 1.5 33c0-6.039 4.226-11.09 9.882-12.36A15.89 15.89 0 0 1 11 17.167c0-8.745 7.089-15.834 15.833-15.834 7.662 0 14.052 5.441 15.518 12.67.105-.002.21-.003.316-.003C51.41 14 58.5 21.089 58.5 29.833c0 7.66-5.44 14.05-12.667 15.517M39.5 36.167l-9.5-9.5m0 0-9.5 9.5m9.5-9.5v38"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 510 B |
|
|
@ -0,0 +1,24 @@
|
|||
<script lang="ts">
|
||||
import type { Image } from '$routes/gallery/lib/gallery'
|
||||
|
||||
export let image: Image
|
||||
export let openModal: (image: Image) => void
|
||||
|
||||
const handleOpenModal = () => openModal(image)
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative group w-full aspect-[22/23] rounded-lg border-2 border-transparent hover:border-base-content box-border overflow-hidden transition-colors ease-in"
|
||||
on:click={handleOpenModal}
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center absolute z-10 top-0 right-0 bottom-0 left-0 bg-[#00000035] opacity-0 group-hover:opacity-100 transition-opacity ease-in"
|
||||
/>
|
||||
<div class="relative pb-[105%]">
|
||||
<img
|
||||
class="absolute block object-cover object-center w-full h-full"
|
||||
alt={`Gallery Image: ${image.name}`}
|
||||
src={image.src}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
import TerminusClient from "@terminusdb/terminusdb-client"
|
||||
import { filesystemStore, sessionStore } from '$src/stores'
|
||||
import { AREAS, galleryStore } from '$routes/gallery/stores'
|
||||
import { getImagesFromWNFS, type Image } from '$routes/gallery/lib/gallery'
|
||||
import FileUploadCard from '$routes/gallery/components/upload/FileUploadCard.svelte'
|
||||
import ImageCard from '$routes/gallery/components/imageGallery/ImageCard.svelte'
|
||||
import ImageModal from '$routes/gallery/components/imageGallery/ImageModal.svelte'
|
||||
|
||||
const client = new TerminusClient.WOQLClient(
|
||||
"https://cloud.terminusdb.com/Myseelia",{
|
||||
user:"zaldarren@gmail.com",
|
||||
organization:"Myseelia",
|
||||
db: "Myseelia",
|
||||
token: "dGVybWludXNkYjovLy9kYXRhL2tleXNfYXBpLzg5OTY0ZGI5OWFlYjQ1Zjc5OGM5ZTRiZWI2MzExOGJhZjhiOWRiOWNlOTJiNmU2NGI0NDEzZjIzNDFmOGVkMjc=_869e9bd2465ad84126151962994fcfa22d4b7ec9375edf16b4182e7f36e4b2b820075ba22e78f629e0691eddbeae6998a6504d5ce287aa1df2602cb556b58e1730b0b93feb0e9304"
|
||||
}
|
||||
);
|
||||
|
||||
let username = $sessionStore.username;
|
||||
let bioregion = '';
|
||||
let ecozone = '';
|
||||
let affiliatedOrganizations = "Organization/8c8368b55dc80f18ba254771701f6d1bc79a3a90f127c28b3145a2c2204e97ce";
|
||||
let givenName = '';
|
||||
let hasCredential = {};
|
||||
|
||||
|
||||
onMount(async () => {
|
||||
await client.connect()
|
||||
const schema = await client.getSchema("myseelia", "main")
|
||||
});
|
||||
|
||||
/**
|
||||
* Open the ImageModal and pass it the selected `image` from the gallery
|
||||
* @param image
|
||||
*/
|
||||
let selectedImage: Image
|
||||
const setSelectedImage: (image: Image) => void = image =>
|
||||
(selectedImage = image)
|
||||
|
||||
const clearSelectedImage = () => (selectedImage = null)
|
||||
|
||||
// If galleryStore.selectedArea changes from private to public, re-run getImagesFromWNFS
|
||||
let selectedArea = null
|
||||
const unsubscribeGalleryStore = galleryStore.subscribe(async updatedStore => {
|
||||
// Get initial selectedArea
|
||||
if (!selectedArea) {
|
||||
selectedArea = updatedStore.selectedArea
|
||||
}
|
||||
|
||||
if (selectedArea !== updatedStore.selectedArea) {
|
||||
selectedArea = updatedStore.selectedArea
|
||||
await getImagesFromWNFS()
|
||||
}
|
||||
})
|
||||
|
||||
// Once the user has been authed, fetch the images from their file system
|
||||
let imagesFetched = false
|
||||
const unsubscribeSessionStore = sessionStore.subscribe((newState) => {
|
||||
if (newState.authed && $filesystemStore && !imagesFetched) {
|
||||
imagesFetched = true
|
||||
// Get images from the user's public WNFS
|
||||
getImagesFromWNFS()
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribeGalleryStore()
|
||||
unsubscribeSessionStore()
|
||||
})
|
||||
|
||||
|
||||
|
||||
function handleSubmit() {
|
||||
makeConnection();
|
||||
}
|
||||
|
||||
export async function makeConnection(){
|
||||
try{
|
||||
const entryObj =
|
||||
{
|
||||
"@type" : "Person",
|
||||
"userName" : username,
|
||||
"givenName" : givenName,
|
||||
"bioregion": bioregion,
|
||||
"ecozone": ecozone,
|
||||
"hasCredential": hasCredential,
|
||||
"affiliation": affiliatedOrganizations
|
||||
};
|
||||
if (username == entryObj.userName){
|
||||
await client.updateDocument(entryObj)
|
||||
} else{
|
||||
await client.addDocument(entryObj);
|
||||
}
|
||||
const entries2 = await client.getDocument({"graph_type":"instance","as_list":true,"type":"Person"})
|
||||
console.log(entries2);
|
||||
}catch(err){
|
||||
console.error(err.message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
form {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 3fr;
|
||||
}
|
||||
|
||||
label {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
input{
|
||||
background-color: rgb(255, 255, 255);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button{
|
||||
background-color: #4CAF50; /* Green */
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<section class="overflow-hidden text-gray-700">
|
||||
<div class="pt-8 p-6 md:p-8 mx-auto">
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:lg:grid-cols-6 gap-4"
|
||||
>
|
||||
{#each $galleryStore.selectedArea === AREAS.PRIVATE ? $galleryStore.privateImages : $galleryStore.publicImages as image}{/each}
|
||||
</div>
|
||||
<form on:submit|preventDefault={handleSubmit}>
|
||||
<label class="label dark:text-white">
|
||||
Given Name:
|
||||
<input class="input text-white dark:text-black" type="text" bind:value={givenName} />
|
||||
</label>
|
||||
<br />
|
||||
<label class="label dark:text-white">
|
||||
Bioregion:
|
||||
<input class="input text-white dark:text-black" type="text" bind:value={bioregion} />
|
||||
</label>
|
||||
<br />
|
||||
<label class="label dark:text-white">
|
||||
Ecozone:
|
||||
<input class="input text-white dark:text-black" type="text" bind:value={ecozone} />
|
||||
</label >
|
||||
<br />
|
||||
<label class="label dark:text-white">
|
||||
Has Credential:
|
||||
<input class="input text-white dark:text-black" type="text" bind:value={hasCredential} />
|
||||
</label >
|
||||
<br />
|
||||
<label class="label dark:text-white">
|
||||
Affiliated organizations:
|
||||
<input class="input text-white dark:text-black" type="text" bind:value={affiliatedOrganizations}/>
|
||||
</label>
|
||||
<br />
|
||||
<button class="bg-blue-500 text-white dark:text-black" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{#if selectedImage}
|
||||
<ImageModal
|
||||
image={selectedImage}
|
||||
isModalOpen={!!selectedImage}
|
||||
on:close={clearSelectedImage}
|
||||
/>
|
||||
{/if}
|
||||
</section>
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
|
||||
import { ipfsGatewayUrl } from '$lib/app-info';
|
||||
import { galleryStore } from '$routes/gallery/stores'
|
||||
import { deleteImageFromWNFS, type Gallery, type Image } from '$routes/gallery/lib/gallery'
|
||||
|
||||
export let image: Image
|
||||
export let isModalOpen: boolean = false
|
||||
let previousImage: Image | undefined
|
||||
let nextImage: Image | undefined
|
||||
let showPreviousArrow: boolean
|
||||
let showNextArrow: boolean
|
||||
let gallery: Gallery
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const unsubcribe = galleryStore.subscribe(newState => (gallery = newState))
|
||||
|
||||
/**
|
||||
* Close the modal, clear the image state vars, set `isModalOpen` to false
|
||||
* and dispatch the close event to clear the image from the parent's state
|
||||
*/
|
||||
const handleCloseModal: () => void = () => {
|
||||
image = null
|
||||
previousImage = null
|
||||
nextImage = null
|
||||
isModalOpen = false
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an image from the user's WNFS
|
||||
*/
|
||||
const handleDeleteImage: () => Promise<void> = async () => {
|
||||
await deleteImageFromWNFS(image.name)
|
||||
handleCloseModal()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the previous and next images to be toggled to when the arrows are clicked
|
||||
*/
|
||||
const setCarouselState = () => {
|
||||
const imageList = image.private
|
||||
? gallery.privateImages
|
||||
: gallery.publicImages
|
||||
const currentIndex = imageList.findIndex(val => val.cid === image.cid)
|
||||
previousImage =
|
||||
imageList[currentIndex - 1] ?? imageList[imageList.length - 1]
|
||||
nextImage = imageList[currentIndex + 1] ?? imageList[0]
|
||||
|
||||
showPreviousArrow = imageList.length > 1 && !!previousImage
|
||||
showNextArrow = imageList.length > 1 && !!nextImage
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the correct image when a user clicks the Next or Previous arrows
|
||||
* @param direction
|
||||
*/
|
||||
const handleNextOrPrevImage: (
|
||||
direction: 'next' | 'prev'
|
||||
) => void = direction => {
|
||||
image = direction === 'prev' ? previousImage : nextImage
|
||||
setCarouselState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect `Escape` key presses to close the modal or `ArrowRight`/`ArrowLeft`
|
||||
* presses to navigate the carousel
|
||||
* @param event
|
||||
*/
|
||||
const handleKeyDown: (event: KeyboardEvent) => void = event => {
|
||||
if (event.key === 'Escape') handleCloseModal()
|
||||
|
||||
if (showNextArrow && event.key === 'ArrowRight')
|
||||
handleNextOrPrevImage('next')
|
||||
|
||||
if (showPreviousArrow && event.key === 'ArrowLeft')
|
||||
handleNextOrPrevImage('prev')
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
setCarouselState()
|
||||
})
|
||||
|
||||
// Unsubscribe from galleryStore updates
|
||||
onDestroy(unsubcribe)
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
|
||||
{#if !!image}
|
||||
<!-- bind:checked can't be set to !!image, so we need to set it to a boolean(casting image as a boolean throws a svelte error, so we're using isModalOpen) -->
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`image-modal-${image.cid}`}
|
||||
class="modal-toggle"
|
||||
bind:checked={isModalOpen}
|
||||
/>
|
||||
<label
|
||||
for={`image-modal-${image.cid}`}
|
||||
class="modal cursor-pointer z-50"
|
||||
on:click|self={handleCloseModal}
|
||||
>
|
||||
<div class="modal-box relative text-center text-base-content">
|
||||
<label
|
||||
for={`image-modal-${image.cid}`}
|
||||
class="btn btn-xs btn-circle absolute right-2 top-2"
|
||||
on:click={handleCloseModal}
|
||||
>
|
||||
✕
|
||||
</label>
|
||||
<div>
|
||||
<h3 class="mb-7 text-lg break-all">{image.name}</h3>
|
||||
|
||||
<div class="relative">
|
||||
{#if showPreviousArrow}
|
||||
<button
|
||||
class="absolute top-1/2 -left-[25px] -translate-y-1/2 inline-block text-center text-[40px]"
|
||||
on:click={() => handleNextOrPrevImage('prev')}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
{/if}
|
||||
<img
|
||||
class="block object-cover object-center border-2 border-base-content w-full h-full mb-4 rounded-[1rem]"
|
||||
alt={`Image: ${image.name}`}
|
||||
src={image.src}
|
||||
/>
|
||||
{#if showNextArrow}
|
||||
<button
|
||||
class="absolute top-1/2 -right-[25px] -translate-y-1/2 inline-block text-center text-[40px]"
|
||||
on:click={() => handleNextOrPrevImage('next')}
|
||||
>
|
||||
›
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<a
|
||||
href={`https://ipfs.${ipfsGatewayUrl}/ipfs/${image.cid}/userland`}
|
||||
target="_blank"
|
||||
class="underline mb-4 hover:text-slate-500"
|
||||
>
|
||||
View on IPFS
|
||||
</a>
|
||||
<p class="mb-4">
|
||||
Created at {new Date(image.ctime).toDateString()}
|
||||
</p>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<a href={image.src} download={image.name} class="btn btn-primary">
|
||||
Download Image
|
||||
</a>
|
||||
<button class="btn btn-outline" on:click={handleDeleteImage}>
|
||||
Delete Image
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
{/if}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
<script lang="ts">
|
||||
import TerminusClient from "@terminusdb/terminusdb-client";
|
||||
import { onDestroy } from 'svelte'
|
||||
|
||||
import { filesystemStore, sessionStore } from '$src/stores'
|
||||
import { AREAS, galleryStore } from '$routes/gallery/stores'
|
||||
import { getImagesFromWNFS, type Image } from '$routes/gallery/lib/gallery'
|
||||
import FileUploadCard from '$routes/gallery/components/upload/FileUploadCard.svelte'
|
||||
import ImageCard from '$routes/gallery/components/imageGallery/ImageCard.svelte'
|
||||
import ImageModal from '$routes/gallery/components/imageGallery/ImageModal.svelte'
|
||||
|
||||
/**
|
||||
* Open the ImageModal and pass it the selected `image` from the gallery
|
||||
* @param image
|
||||
*/
|
||||
let selectedImage: Image
|
||||
const setSelectedImage: (image: Image) => void = image =>
|
||||
(selectedImage = image)
|
||||
|
||||
// If galleryStore.selectedArea changes from private to public, re-run getImagesFromWNFS
|
||||
let selectedArea = null
|
||||
const unsubscribeGalleryStore = galleryStore.subscribe(async updatedStore => {
|
||||
// Get initial selectedArea
|
||||
if (!selectedArea) {
|
||||
selectedArea = updatedStore.selectedArea
|
||||
}
|
||||
|
||||
if (selectedArea !== updatedStore.selectedArea) {
|
||||
selectedArea = updatedStore.selectedArea
|
||||
}
|
||||
})
|
||||
|
||||
// Once the user has been authed, fetch the images from their file system
|
||||
let imagesFetched = false
|
||||
const unsubscribeSessionStore = sessionStore.subscribe((newState) => {
|
||||
if (newState.authed && $filesystemStore && !imagesFetched) {
|
||||
imagesFetched = true
|
||||
// Get images from the user's public WNFS
|
||||
getImagesFromWNFS()
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribeGalleryStore()
|
||||
unsubscribeSessionStore()
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<section class="overflow-hidden text-gray-700">
|
||||
<div class="pt-8 p-6 md:p-8 mx-auto">
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:lg:grid-cols-6 gap-4"
|
||||
>
|
||||
<FileUploadCard />
|
||||
{#each $galleryStore.selectedArea === AREAS.PRIVATE ? $galleryStore.privateImages : $galleryStore.publicImages as image}
|
||||
<ImageCard {image} openModal={setSelectedImage} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if selectedImage}
|
||||
<ImageModal
|
||||
image={selectedImage}
|
||||
isModalOpen={!!selectedImage}
|
||||
on:close={clearSelectedImage}
|
||||
/>
|
||||
{/if}
|
||||
</section>
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
getImagesFromWNFS,
|
||||
uploadImageToWNFS
|
||||
} from '$routes/gallery/lib/gallery'
|
||||
import { addNotification } from '$lib/notifications'
|
||||
|
||||
/**
|
||||
* Detect when a user drags a file in or out of the dropzone to change the styles
|
||||
*/
|
||||
let isDragging = false
|
||||
const handleDragEnter: () => void = () => (isDragging = true)
|
||||
const handleDragLeave: () => void = () => (isDragging = false)
|
||||
|
||||
/**
|
||||
* Process files being dropped in the drop zone and ensure they are images
|
||||
* @param event
|
||||
*/
|
||||
const handleDrop: (event: DragEvent) => Promise<void> = async event => {
|
||||
// Prevent default behavior (Prevent file from being opened)
|
||||
event.preventDefault()
|
||||
|
||||
const files = Array.from(event.dataTransfer.items)
|
||||
|
||||
// Iterate over the dropped files and upload them to WNFS
|
||||
await Promise.all(
|
||||
files.map(async item => {
|
||||
if (item.kind === 'file') {
|
||||
const file: File = item.getAsFile()
|
||||
|
||||
// If the dropped files aren't images, we don't want them!
|
||||
if (!file.type.match('image/*')) {
|
||||
addNotification('Please upload images only', 'error')
|
||||
console.error('Please upload images only')
|
||||
} else {
|
||||
await uploadImageToWNFS(file)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// Refetch images and update galleryStore
|
||||
await getImagesFromWNFS()
|
||||
|
||||
// Disable isDragging state
|
||||
isDragging = false
|
||||
}
|
||||
|
||||
/**
|
||||
* This is needed to prevent the default behaviour of the file opening in browser
|
||||
* when it is dropped
|
||||
* @param event
|
||||
*/
|
||||
const handleDragOver: (event: DragEvent) => void = event =>
|
||||
event.preventDefault()
|
||||
</script>
|
||||
|
||||
<label
|
||||
on:drop={handleDrop}
|
||||
on:dragover={handleDragOver}
|
||||
on:dragenter={handleDragEnter}
|
||||
on:dragleave={handleDragLeave}
|
||||
for="dropzone-file"
|
||||
class="block w-full min-h-[calc(100vh-190px)] rounded-lg border-2 border-solid border-base-content transition ease-in cursor-pointer {isDragging
|
||||
? 'border-dashed !border-orange-700 bg-orange-50'
|
||||
: ''}"
|
||||
>
|
||||
<slot />
|
||||
</label>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<script lang="ts">
|
||||
import { galleryStore } from '$routes/gallery/stores'
|
||||
import { handleFileInput } from '$routes/gallery/lib/gallery'
|
||||
import FileUploadIcon from '$routes/gallery/components/icons/FileUploadIcon.svelte'
|
||||
|
||||
// Handle files uploaded directly through the file input
|
||||
let files: FileList
|
||||
$: if (files) {
|
||||
handleFileInput(files)
|
||||
}
|
||||
</script>
|
||||
|
||||
<label
|
||||
for="upload-file"
|
||||
class="group btn !p-0 !h-auto flex flex-col justify-center items-center aspect-[22/23] object-cover rounded-lg shadow-orange hover:border-neutral-50 overflow-hidden transition-colors ease-in bg-base-100 border-2 box-content border-neutral cursor-pointer text-neutral bg-gradient-to-r from-orange-600 to-orange-300"
|
||||
>
|
||||
{#if $galleryStore.loading}
|
||||
<div class="flex justify-center items-center p-12">
|
||||
<div
|
||||
class="loader ease-linear rounded-full border-4 border-t-4 border-t-orange-300 border-neutral h-16 w-16 animate-spin"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col justify-center items-center pt-5 pb-6">
|
||||
<FileUploadIcon />
|
||||
<p class="mt-4 mb-2 text-sm">
|
||||
<span class="font-bold text-sm">Upload a photo</span>
|
||||
</p>
|
||||
<p class="text-xxs">SVG, PNG, JPG or GIF</p>
|
||||
</div>
|
||||
<input
|
||||
bind:files
|
||||
id="upload-file"
|
||||
type="file"
|
||||
multiple
|
||||
accept="image/*"
|
||||
class="hidden"
|
||||
/>
|
||||
{/if}
|
||||
</label>
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
{
|
||||
"entities": [{
|
||||
"label": "Organization",
|
||||
"title": "Neuralink"
|
||||
}, {
|
||||
"label": "Organization",
|
||||
"title": "SpaceX"
|
||||
}, {
|
||||
"label": "Organization",
|
||||
"title": "Pretoria"
|
||||
}, {
|
||||
"label": "Organization",
|
||||
"title": "The Boring Company"
|
||||
}, {
|
||||
"label": "Organization",
|
||||
"title": "University of Pretoria"
|
||||
}, {
|
||||
"label": "Organization",
|
||||
"title": "Stanford University"
|
||||
}, {
|
||||
"label": "Person",
|
||||
"title": "Jeff Bezos"
|
||||
}, {
|
||||
"label": "Organization",
|
||||
"title": "University of Pennsylvania"
|
||||
}, {
|
||||
"label": "Person",
|
||||
"title": "Kimbal Musk"
|
||||
}, {
|
||||
"label": "Organization",
|
||||
"title": "Tesla, Inc."
|
||||
}, {
|
||||
"label": "Person",
|
||||
"title": "Elon Musk"
|
||||
}],
|
||||
"relations": [{
|
||||
"source": "Elon Musk",
|
||||
"target": "Neuralink"
|
||||
}, {
|
||||
"source": "Tesla, Inc.",
|
||||
"target": "Elon Musk",
|
||||
"type": "owned by"
|
||||
}, {
|
||||
"source": "Elon Musk",
|
||||
"target": "University of Pennsylvania",
|
||||
"type": "residence"
|
||||
}, {
|
||||
"source": "Elon Musk",
|
||||
"target": "Tesla, Inc.",
|
||||
"type": "owned by"
|
||||
}, {
|
||||
"source": "The Boring Company",
|
||||
"target": "Tesla, Inc.",
|
||||
"type": "owned by"
|
||||
}, {
|
||||
"source": "Elon Musk",
|
||||
"target": "Kimbal Musk",
|
||||
"type": "sibling"
|
||||
}, {
|
||||
"source": "University of Pennsylvania",
|
||||
"target": "Elon Musk",
|
||||
"type": "residence"
|
||||
}, {
|
||||
"source": "The Boring Company",
|
||||
"target": "Neuralink",
|
||||
"type": "subsidiary"
|
||||
}, {
|
||||
"source": "Elon Musk",
|
||||
"target": "University of Pretoria",
|
||||
"type": "work location"
|
||||
}, {
|
||||
"source": "The Boring Company",
|
||||
"target": "Elon Musk",
|
||||
"type": "owned by"
|
||||
}, {
|
||||
"source": "Kimbal Musk",
|
||||
"target": "Elon Musk",
|
||||
"type": "sibling"
|
||||
}, {
|
||||
"source": "Neuralink",
|
||||
"target": "Elon Musk",
|
||||
"type": "owned by"
|
||||
}, {
|
||||
"source": "Elon Musk",
|
||||
"target": "The Boring Company",
|
||||
"type": "owned by"
|
||||
}, {
|
||||
"source": "Elon Musk",
|
||||
"target": "University of Pennsylvania",
|
||||
"type": "work location"
|
||||
}]
|
||||
}
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
import { get as getStore } from 'svelte/store'
|
||||
import * as wn from 'webnative'
|
||||
import * as uint8arrays from 'uint8arrays'
|
||||
import type { CID } from 'multiformats/cid'
|
||||
import type { PuttableUnixTree, File as WNFile } from 'webnative/fs/types'
|
||||
import type { Metadata } from 'webnative/fs/metadata'
|
||||
|
||||
import { filesystemStore } from '$src/stores'
|
||||
import { AREAS, galleryStore } from '$routes/gallery/stores'
|
||||
import { addNotification } from '$lib/notifications'
|
||||
|
||||
export type Image = {
|
||||
cid: string
|
||||
ctime: number
|
||||
name: string
|
||||
private: boolean
|
||||
size: number
|
||||
src: string
|
||||
}
|
||||
|
||||
export type Gallery = {
|
||||
publicImages: Image[] | null
|
||||
privateImages: Image[] | null
|
||||
selectedArea: AREAS
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
interface GalleryFile extends PuttableUnixTree, WNFile {
|
||||
cid: CID
|
||||
content: Uint8Array
|
||||
header: {
|
||||
content: Uint8Array
|
||||
metadata: Metadata
|
||||
}
|
||||
}
|
||||
|
||||
type Link = {
|
||||
size: number
|
||||
}
|
||||
|
||||
export const GALLERY_DIRS = {
|
||||
[AREAS.PUBLIC]: ['public', 'gallery'],
|
||||
[AREAS.PRIVATE]: ['private', 'gallery']
|
||||
}
|
||||
const FILE_SIZE_LIMIT = 5
|
||||
|
||||
/**
|
||||
* Get images from the user's WNFS and construct the `src` value for the images
|
||||
*/
|
||||
export const getImagesFromWNFS: () => Promise<void> = async () => {
|
||||
try {
|
||||
// Set loading: true on the galleryStore
|
||||
galleryStore.update(store => ({ ...store, loading: true }))
|
||||
|
||||
const { selectedArea } = getStore(galleryStore)
|
||||
const isPrivate = selectedArea === AREAS.PRIVATE
|
||||
const fs = getStore(filesystemStore)
|
||||
|
||||
// Set path to either private or public gallery dir
|
||||
const path = wn.path.directory(...GALLERY_DIRS[selectedArea])
|
||||
|
||||
// Get list of links for files in the gallery dir
|
||||
const links = await fs.ls(path)
|
||||
|
||||
const images = await Promise.all(
|
||||
Object.entries(links).map(async ([name]) => {
|
||||
const file = await fs.get(
|
||||
wn.path.file(...GALLERY_DIRS[selectedArea], `${name}`)
|
||||
)
|
||||
|
||||
// The CID for private files is currently located in `file.header.content`,
|
||||
// whereas the CID for public files is located in `file.cid`
|
||||
const cid = isPrivate
|
||||
? (file as GalleryFile).header.content.toString()
|
||||
: (file as GalleryFile).cid.toString()
|
||||
|
||||
// Create a base64 string to use as the image `src`
|
||||
const src = `data:image/jpeg;base64, ${uint8arrays.toString(
|
||||
(file as GalleryFile).content,
|
||||
'base64'
|
||||
)}`
|
||||
|
||||
return {
|
||||
cid,
|
||||
ctime: (file as GalleryFile).header.metadata.unixMeta.ctime,
|
||||
name,
|
||||
private: isPrivate,
|
||||
size: (links[name] as Link).size,
|
||||
src
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// Sort images by ctime(created at date)
|
||||
// NOTE: this will eventually be controlled via the UI
|
||||
images.sort((a, b) => b.ctime - a.ctime)
|
||||
|
||||
// Push images to the galleryStore
|
||||
galleryStore.update(store => ({
|
||||
...store,
|
||||
...(isPrivate
|
||||
? {
|
||||
privateImages: images
|
||||
}
|
||||
: {
|
||||
publicImages: images
|
||||
}),
|
||||
loading: false
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
galleryStore.update(store => ({
|
||||
...store,
|
||||
loading: false
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload an image to the user's private or public WNFS
|
||||
* @param image
|
||||
*/
|
||||
export const uploadImageToWNFS: (
|
||||
image: File
|
||||
) => Promise<void> = async image => {
|
||||
try {
|
||||
const { selectedArea } = getStore(galleryStore)
|
||||
const fs = getStore(filesystemStore)
|
||||
|
||||
// Reject files over 5MB
|
||||
const imageSizeInMB = image.size / (1024 * 1024)
|
||||
if (imageSizeInMB > FILE_SIZE_LIMIT) {
|
||||
throw new Error('Image can be no larger than 5MB')
|
||||
}
|
||||
|
||||
// Reject the upload if the image already exists in the directory
|
||||
const imageExists = await fs.exists(
|
||||
wn.path.file(...GALLERY_DIRS[selectedArea], image.name)
|
||||
)
|
||||
if (imageExists) {
|
||||
throw new Error(`${image.name} image already exists`)
|
||||
}
|
||||
|
||||
// Create a sub directory and add some content
|
||||
await fs.write(
|
||||
wn.path.file(...GALLERY_DIRS[selectedArea], image.name),
|
||||
image
|
||||
)
|
||||
|
||||
// Announce the changes to the server
|
||||
await fs.publish()
|
||||
|
||||
addNotification(`${image.name} image has been published`, 'success')
|
||||
} catch (error) {
|
||||
addNotification(error.message, 'error')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an image from the user's private or public WNFS
|
||||
* @param name
|
||||
*/
|
||||
export const deleteImageFromWNFS: (
|
||||
name: string
|
||||
) => Promise<void> = async name => {
|
||||
try {
|
||||
const { selectedArea } = getStore(galleryStore)
|
||||
const fs = getStore(filesystemStore)
|
||||
|
||||
const imageExists = await fs.exists(
|
||||
wn.path.file(...GALLERY_DIRS[selectedArea], name)
|
||||
)
|
||||
|
||||
if (imageExists) {
|
||||
// Remove images from server
|
||||
await fs.rm(wn.path.file(...GALLERY_DIRS[selectedArea], name))
|
||||
|
||||
// Announce the changes to the server
|
||||
await fs.publish()
|
||||
|
||||
addNotification(`${name} image has been deleted`, 'success')
|
||||
|
||||
// Refetch images and update galleryStore
|
||||
await getImagesFromWNFS()
|
||||
} else {
|
||||
throw new Error(`${name} image has already been deleted`)
|
||||
}
|
||||
} catch (error) {
|
||||
addNotification(error.message, 'error')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle uploads made by interacting with the file input directly
|
||||
*/
|
||||
export const handleFileInput: (
|
||||
files: FileList
|
||||
) => Promise<void> = async files => {
|
||||
await Promise.all(
|
||||
Array.from(files).map(async file => {
|
||||
await uploadImageToWNFS(file)
|
||||
})
|
||||
)
|
||||
|
||||
// Refetch images and update galleryStore
|
||||
await getImagesFromWNFS()
|
||||
}
|
||||
Loading…
Reference in New Issue